JavaRush /Blog Java /Random-PL /Wielowątkowość w Javie: istota, zalety i typowe pułapki

Wielowątkowość w Javie: istota, zalety i typowe pułapki

Opublikowano w grupie Random-PL
Cześć! Przede wszystkim gratulacje: dotarłeś do tematu wielowątkowości w Javie! To poważne osiągnięcie, przed nami jeszcze długa droga. Ale przygotuj się: to jeden z najtrudniejszych tematów na kursie. I nie chodzi o to, że stosuje się tu złożone klasy czy wiele metod: wręcz przeciwnie, nie ma ich nawet dwa tuziny. Chodzi raczej o to, że musisz trochę zmienić swoje myślenie. Poprzednio programy były wykonywane sekwencyjnie. Niektóre linie kodu podążały za innymi, niektóre metody za innymi i ogólnie wszystko było jasne. Najpierw coś oblicz, następnie wyświetl wynik na konsoli, a następnie zakończ program. Aby zrozumieć wielowątkowość, najlepiej myśleć w kategoriach współbieżności. Zacznijmy od czegoś bardzo prostego :) Wielowątkowość w Javie: istota, zalety i typowe pułapki - 1Wyobraź sobie, że Twoja rodzina przeprowadza się z jednego domu do drugiego. Ważną częścią przeprowadzki jest pakowanie książek. Zgromadziłeś dużo książek i musisz je umieścić w pudełkach. Teraz tylko ty jesteś wolny. Mama przygotowuje jedzenie, brat zbiera ubrania, a siostra poszła do sklepu. W pojedynkę dasz radę przynajmniej i prędzej czy później nawet sam wykonasz zadanie, ale zajmie to dużo czasu. Jednak za 20 minut Twoja siostra wróci ze sklepu i nie będzie miała nic innego do roboty. Żeby mogła do ciebie dołączyć. Zadanie pozostało takie samo: włożyć książki do pudeł. Po prostu działa dwa razy szybciej. Dlaczego? Ponieważ praca jest wykonywana równolegle. Dwie różne „nitki” (Ty i Twoja siostra) wykonują jednocześnie to samo zadanie i jeśli nic się nie zmieni, różnica czasu będzie bardzo duża w porównaniu do sytuacji, w której zrobiłbyś wszystko sam. Jeśli twój brat wkrótce wykona swoje zadanie, będzie mógł ci pomóc, a sprawy potoczą się jeszcze szybciej.

Problemy, które wielowątkowość rozwiązuje w Javie

Zasadniczo wielowątkowość Java została wynaleziona, aby rozwiązać dwa główne problemy:
  1. Wykonuj wiele czynności jednocześnie.

    W powyższym przykładzie różne wątki (czyli członkowie rodziny) wykonywali kilka czynności równolegle: umyli naczynia, poszli do sklepu, złożyli rzeczy.

    Można podać przykład bardziej „programistyczny”. Wyobraź sobie, że masz program z interfejsem użytkownika. Po kliknięciu przycisku Kontynuuj w programie powinny nastąpić pewne obliczenia, a użytkownikowi powinien wyświetlić się następujący ekran interfejsu. Jeśli te czynności będą wykonywane sekwencyjnie, po kliknięciu przycisku „Kontynuuj” program po prostu się zawiesi. Użytkownik zobaczy ten sam ekran z przyciskiem „Kontynuuj”, aż do zakończenia wszystkich wewnętrznych obliczeń i dotarcia programu do części, w której rozpocznie się rysowanie interfejsu.

    Cóż, poczekajmy kilka minut!

    Wielowątkowość w Javie: istota, zalety i typowe pułapki - 3

    Możemy także przerobić nasz program lub, jak mówią programiści, „równolegle”. Niech niezbędne obliczenia zostaną wykonane w jednym wątku, a renderowanie interfejsu w innym. Większość komputerów ma do tego wystarczające zasoby. W tym przypadku program nie będzie „głupi”, a użytkownik będzie spokojnie poruszał się pomiędzy ekranami interfejsu, nie przejmując się tym, co dzieje się w środku. Nie przeszkadza :)

  2. Przyspiesz obliczenia.

    Tutaj wszystko jest znacznie prostsze. Jeśli nasz procesor ma kilka rdzeni, a większość procesorów jest teraz wielordzeniowych, naszą listę zadań można rozwiązać równolegle przez kilka rdzeni. Oczywiście, jeśli musimy rozwiązać 1000 problemów, a każdy z nich zostanie rozwiązany w ciągu sekundy, jeden rdzeń poradzi sobie z listą w 1000 sekund, dwa rdzenie w 500 sekund, trzy w nieco ponad 333 sekundy i tak dalej.

Ale jak już przeczytałeś na wykładzie, współczesne systemy są bardzo inteligentne i nawet na jednym rdzeniu obliczeniowym są w stanie zaimplementować równoległość, czyli pseudorównoległość, gdy zadania wykonywane są naprzemiennie. Przejdźmy od spraw ogólnych do szczegółowych i zapoznajmy się z główną klasą w bibliotece Java związaną z wielowątkowością - java.lang.Thread. Ściśle mówiąc, wątki w Javie są reprezentowane przez instancje klasy Thread. Oznacza to, że aby utworzyć i uruchomić 10 wątków, będziesz potrzebować 10 obiektów tej klasy. Napiszmy najprostszy przykład:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
Aby tworzyć i uruchamiać wątki, musimy utworzyć klasę i dziedziczyć ją z java.lang. Threadi zastąp znajdującą się w nim metodę run(). To ostatnie jest bardzo ważne. To w metodzie run()określamy logikę, którą musi wykonać nasz wątek. Teraz, jeśli utworzymy instancję MyFirstThreadi ją uruchomimy, metoda run()wypisze do konsoli linię z jej nazwą: metoda getName()wypisuje „systemową” nazwę wątku, która jest przydzielana automatycznie. Chociaż właściwie, dlaczego „jeśli”? Twórzmy i testujmy!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
Dane wyjściowe konsoli: Jestem wątkiem! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-7. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-8. Tworzymy 10 wątków (obiektów) MyFirstThread, które dziedziczą Threadi uruchamiają je, wywołując metodę obiektu start(). Po wywołaniu metody , start()jej metoda zaczyna działać run()i wykonywana jest logika w niej zapisana. Uwaga: nazwy wątków nie są uporządkowane. To dość dziwne, dlaczego nie wykonano ich po kolei: Thread-0, Thread-1, Thread-2i tak dalej? To jest dokładnie przykład sytuacji, w której standardowe, „sekwencyjne” myślenie nie zadziała. Fakt jest taki, że w tym przypadku wydajemy jedynie polecenia utworzenia i uruchomienia 10 wątków. O kolejności ich uruchamiania decyduje harmonogram wątków: specjalny mechanizm wewnątrz systemu operacyjnego. To, jak dokładnie jest ona zorganizowana i na jakich zasadach podejmuje decyzje, jest bardzo złożonym tematem i nie będziemy się teraz w to zagłębiać. Najważniejszą rzeczą do zapamiętania jest to, że programista nie może kontrolować kolejności wykonywania wątków. Aby zdać sobie sprawę z powagi sytuacji, spróbuj main()jeszcze kilka razy uruchomić metodę z powyższego przykładu. Drugie wyjście konsoli: Jestem wątkiem! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-8. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-7. Trzecie wyjście konsoli: Jestem Thread! Nazywam się Thread-0. Jestem Thread! Nazywam się Thread-3. Jestem Thread! Nazywam się Thread-1. Jestem Thread! Nazywam się Thread-2. Jestem Thread! Nazywam się Thread-6. Jestem Thread! Nazywam się Thread-4. Jestem Thread! Nazywam się Thread-9. Jestem Thread! Nazywam się Thread-5. Jestem Thread! Nazywam się Thread-7. Jestem Thread! Nazywam się Thread-8

Problemy stwarzane przez wielowątkowość

Na przykładzie z książkami widziałeś, że wielowątkowość rozwiązuje dość istotne problemy, a jej zastosowanie przyspiesza pracę naszych programów. W wielu przypadkach – wielokrotnie. Ale nie bez powodu wielowątkowość jest uważana za złożony temat. W końcu, jeśli jest niewłaściwie używany, zamiast go rozwiązywać, stwarza problemy. Kiedy mówię „tworzyć problemy”, nie mam na myśli czegoś abstrakcyjnego. Istnieją dwa specyficzne problemy, które może powodować wielowątkowość: zakleszczenie i sytuacja wyścigowa. Zakleszczenie to sytuacja, w której wiele wątków oczekuje na zajęte przez siebie zasoby i żaden z nich nie może kontynuować wykonywania. Porozmawiamy o tym więcej w przyszłych wykładach, ale na razie wystarczy ten przykład: Wielowątkowość w Javie: istota, zalety i typowe pułapki - 4 wyobraźmy sobie, że wątek-1 współpracuje z jakimś Obiektem-1, a wątek-2 pracuje z Obiektem-2. Program jest napisany w następujący sposób:
  1. Wątek-1 przestanie działać z Obiektem-1 i przełączy się na Obiekt-2, gdy tylko Wątek-2 przestanie działać z Obiektem 2 i przełączy się na Obiekt-1.
  2. Wątek-2 przestanie działać z Obiektem-2 i przełączy się na Obiekt-1, gdy tylko Wątek-1 przestanie działać z Obiektem 1 i przełączy się na Obiekt-2.
Nawet bez głębokiej wiedzy na temat wielowątkowości można łatwo zrozumieć, że nic z tego nie wyjdzie. Nici nigdy nie zamienią się miejscami i będą na siebie czekać wiecznie. Błąd wydaje się oczywisty, ale w rzeczywistości taki nie jest. Możesz łatwo wpuścić go do programu. W kolejnych wykładach przyjrzymy się przykładom kodu powodującego zakleszczenie. Swoją drogą, Quora ma doskonały przykład z życia wzięty, wyjaśniający, czym jest impas . „W niektórych stanach Indii nie sprzedadzą ci gruntów rolnych, jeśli nie jesteś zarejestrowany jako rolnik. Jeżeli jednak nie posiadasz gruntów rolnych, nie będziesz zarejestrowany jako rolnik.” Świetnie, co mogę powiedzieć! :) Teraz o stanie wyścigu - stanie wyścigu. Wielowątkowość w Javie: istota, zalety i typowe pułapki - 5Sytuacja wyścigu to wada projektowa wielowątkowego systemu lub aplikacji, w której działanie systemu lub aplikacji zależy od kolejności wykonywania części kodu. Zapamiętaj przykład z działającymi wątkami:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
A teraz wyobraź sobie, że program odpowiada za obsługę robota przygotowującego jedzenie! Thread-0 wyjmuje jajka z lodówki. Strumień 1 włącza piec. Stream-2 wyjmuje patelnię i kładzie ją na kuchence. Stream 3 rozpala ogień w piecu. Strumień 4 wlewa olej na patelnię. Strumień 5 rozbija jajka i wlewa je na patelnię. Strumień 6 wrzuca naboje do kosza na śmieci. Stream-7 usuwa gotową jajecznicę z ognia. Potok-8 nakłada na talerz jajecznicę. Strumień 9 zmywa naczynia. Spójrz na wyniki naszego programu: Wątek-0 wykonany Wątek-2 wykonany Wątek-1 wątek wykonany Wątek-4 wykonany Wątek-9 wykonany Wątek-5 wykonany Wątek-8 wykonany Wątek-7 wykonany wątek Wątek-7 wątek wykonany -3 Wykonany wątek 6. Czy skrypt jest fajny? :) A wszystko dlatego, że działanie naszego programu uzależnione jest od kolejności wykonywania wątków. Przy najmniejszym naruszeniu sekwencji nasza kuchnia zamienia się w piekło, a oszalały robot niszczy wszystko wokół. Jest to również częsty problem w programowaniu wielowątkowym, o którym usłyszysz nie raz. Na koniec wykładu chciałbym polecić Państwu książkę o wielowątkowości.
Wielowątkowość w Javie: istota, zalety i typowe pułapki - 6
Książka „Współbieżność Java w praktyce” została napisana w 2006 roku, ale nie straciła na aktualności. Omówiono w nim programowanie wielowątkowe w Javie, zaczynając od podstaw, a kończąc na liście najczęstszych błędów i antywzorców. Jeśli kiedykolwiek zdecydujesz się zostać guru programowania wielowątkowego, ta książka jest lekturą obowiązkową. Do zobaczenia na kolejnych wykładach! :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION