Java 101: Zrozumienie wątków Java, Część 3: Planowanie wątków i oczekiwanie / powiadamianie

W tym miesiącu kontynuuję moje czteroczęściowe wprowadzenie do wątków Java, koncentrując się na planowaniu wątków, mechanizmie oczekiwania / powiadamiania i przerywaniu wątków. Zbadasz, w jaki sposób maszyna JVM lub program planujący wątki systemu operacyjnego wybiera następny wątek do wykonania. Jak się przekonasz, priorytet jest ważny przy wyborze harmonogramu wątków. Dowiesz się, jak wątek czeka, aż otrzyma powiadomienie z innego wątku, zanim będzie kontynuował wykonywanie, i dowiesz się, jak używać mechanizmu czekania / powiadamiania do koordynowania wykonywania dwóch wątków w relacji producent-konsument. Na koniec dowiesz się, jak przedwcześnie obudzić śpiącą lub oczekującą wątek na zakończenie wątku lub inne zadania. Nauczę Cię również, jak wątek, który nie śpi ani nie czeka, wykrywa żądanie przerwania z innego wątku.

Zwróć uwagę, że ten artykuł (część archiwów JavaWorld) został zaktualizowany o nowe listy kodów i kod źródłowy do pobrania w maju 2013.

Zrozumieć wątki Java - przeczytaj całą serię

  • Część 1: Wprowadzenie wątków i elementów wykonawczych
  • Część 2: Synchronizacja
  • Część 3: Planowanie wątków, oczekiwanie / powiadamianie i przerywanie wątków
  • Część 4: Grupy wątków, zmienność, zmienne lokalne wątku, liczniki czasu i śmierć wątku

Planowanie wątków

W wyidealizowanym świecie wszystkie wątki programu miałyby swoje własne procesory, na których mogłyby działać. Zanim nadejdzie czas, gdy komputery będą miały tysiące lub miliony procesorów, wątki często muszą współdzielić jeden lub więcej procesorów. Maszyna JVM lub system operacyjny platformy odszyfrowuje sposób współdzielenia zasobów procesora między wątkami - jest to zadanie znane jako planowanie wątków . Ta część maszyny JVM lub systemu operacyjnego, która wykonuje planowanie wątków, jest harmonogramem wątków .

Uwaga: aby uprościć dyskusję na temat planowania wątków, skupiam się na planowaniu wątków w kontekście pojedynczego procesora. Możesz ekstrapolować tę dyskusję na wiele procesorów; Zostawiam to zadanie tobie.

Pamiętaj o dwóch ważnych kwestiach dotyczących planowania wątków:

  1. Java nie wymusza na maszynie wirtualnej planowania wątków w określony sposób ani nie zawiera harmonogramu wątków. Oznacza to planowanie wątków zależne od platformy. Dlatego należy zachować ostrożność podczas pisania programu w języku Java, którego zachowanie zależy od sposobu planowania wątków i musi działać spójnie na różnych platformach.
  2. Na szczęście podczas pisania programów w języku Java należy pomyśleć o tym, jak Java planuje wątki tylko wtedy, gdy co najmniej jeden z wątków programu intensywnie wykorzystuje procesor przez długi czas, a pośrednie wyniki wykonania tego wątku są ważne. Na przykład aplet zawiera wątek, który dynamicznie tworzy obraz. Od czasu do czasu chcesz, aby wątek malarski rysował bieżącą zawartość tego obrazu, aby użytkownik mógł zobaczyć, jak postępuje obraz. Aby upewnić się, że wątek obliczeniowy nie zajmuje monopolisty procesora, należy rozważyć planowanie wątków.

Sprawdź program, który tworzy dwa wątki intensywnie wykorzystujące procesor:

Listing 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemotworzy dwa wątki, z których każdy oblicza wartość pi (pięć razy) i wypisuje każdy wynik. W zależności od tego, jak implementacja maszyny JVM planuje wątki, mogą pojawić się wyniki podobne do następujących:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Zgodnie z powyższym wyjściem, program planujący wątki dzieli procesor między oba wątki. Możesz jednak zobaczyć wyniki podobne do tego:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Powyższe dane wyjściowe pokazują, że harmonogram wątków faworyzuje jeden wątek nad drugim. Dwa powyższe dane wyjściowe ilustrują dwie ogólne kategorie programów planujących wątki: zielone i natywne. Zbadam ich różnice w zachowaniu w kolejnych sekcjach. Omawiając każdą kategorię, odwołuję się do stanów wątków, których są cztery:

  1. Stan początkowy: program utworzył obiekt wątku, ale wątek jeszcze nie istnieje, ponieważ start()metoda obiektu wątku nie została jeszcze wywołana.
  2. Stan do uruchomienia : jest to stan domyślny wątku. Po zakończeniu wywołania do start(), wątek staje się gotowy do uruchomienia niezależnie od tego, czy ten wątek jest uruchomiony, czyli używa procesora. Chociaż można uruchomić wiele wątków, obecnie działa tylko jeden. Osoby planujące wątki określają, który działający wątek ma zostać przypisany do procesora.
  3. Zablokowane stan: Kiedy wątek wykonuje sleep(), wait()ani join()metod, podczas próby nitki odczytać dane nie są jeszcze dostępne z sieci, a kiedy wątek czeka uzyskania blokady, że wątek jest w stanie zblokowanym: to nie jest ani systemem ani w stanie biec. (Prawdopodobnie możesz pomyśleć o innych sytuacjach, w których wątek czekałby na coś, co się wydarzy.) Gdy zablokowany wątek zostanie odblokowany, wątek przechodzi do stanu, w którym można uruchomić.
  4. Stan kończący: gdy wykonanie opuszcza run()metodę wątku, ten wątek jest w stanie kończącym. Innymi słowy, nić przestaje istnieć.

W jaki sposób program planujący wątki wybiera uruchamiany wątek do uruchomienia? Zaczynam odpowiadać na to pytanie, omawiając planowanie zielonych wątków. Kończę odpowiedź, omawiając natywne planowanie wątków.

Planowanie zielonego wątku

Nie wszystkie systemy operacyjne, na przykład starożytny system operacyjny Microsoft Windows 3.1, obsługują wątki. W przypadku takich systemów firma Sun Microsystems może zaprojektować maszynę JVM, która dzieli jedyny wątek wykonania na wiele wątków. JVM (a nie system operacyjny platformy) dostarcza logikę wątków i zawiera harmonogram wątków. Wątki JVM to zielone wątki lub wątki użytkownika .

Harmonogram wątków maszyny JVM planuje zielone wątki według priorytetu - względnego znaczenia wątku, które można wyrazić jako liczbę całkowitą z dobrze zdefiniowanego zakresu wartości. Zwykle program planujący wątki maszyny JVM wybiera wątek o najwyższym priorytecie i zezwala na działanie tego wątku do momentu jego zakończenia lub zablokowania. W tym momencie harmonogram wątków wybiera wątek o następnym najwyższym priorytecie. Ten wątek (zwykle) działa do momentu zakończenia lub zablokowania. Jeśli podczas działania wątku wątek o wyższym priorytecie odblokowuje się (być może upłynął czas uśpienia wątku o wyższym priorytecie), program planujący wątki wyprzedza lub przerywa wątek o niższym priorytecie i przypisuje odblokowany wątek o wyższym priorytecie do procesora.

Uwaga: możliwy do uruchomienia wątek o najwyższym priorytecie nie zawsze będzie działał. Oto priorytet specyfikacji języka Java :

Każdy wątek ma priorytet. Gdy występuje konkurencja o zasoby przetwarzania, wątki o wyższym priorytecie są generalnie wykonywane zamiast wątków o niższym priorytecie. Taka preferencja nie gwarantuje jednak, że wątek o najwyższym priorytecie będzie zawsze działał, a priorytetów wątków nie można wykorzystać do niezawodnej implementacji wzajemnego wykluczania.

To stwierdzenie wiele mówi o implementacji maszyn JVM typu green thread. Te maszyny JVM nie mogą pozwolić sobie na blokowanie wątków, ponieważ wiązałoby to jedyny wątek wykonania maszyny JVM. Dlatego też, gdy wątek musi zostać zablokowany, na przykład gdy wątek odczytuje dane powoli, aby dotrzeć do pliku, maszyna JVM może zatrzymać wykonywanie wątku i użyć mechanizmu odpytywania, aby określić, kiedy nadejdą dane. Gdy wątek pozostaje zatrzymany, harmonogram wątków maszyny JVM może zaplanować uruchomienie wątku o niższym priorytecie. Załóżmy, że dane docierają, gdy działa wątek o niższym priorytecie. Chociaż wątek o wyższym priorytecie powinien działać natychmiast po nadejściu danych, nie dzieje się to, dopóki JVM nie sonduje systemu operacyjnego i nie wykryje przybycia. W związku z tym wątek o niższym priorytecie działa, mimo że powinien działać wątek o wyższym priorytecie.Musisz się martwić tą sytuacją tylko wtedy, gdy potrzebujesz zachowania Java w czasie rzeczywistym. Ale Java nie jest systemem operacyjnym czasu rzeczywistego, więc po co się martwić?

Aby zrozumieć, która uruchomiona zielona nić staje się aktualnie działającą zieloną nitką, rozważ następujące kwestie. Załóżmy, że Twoja aplikacja składa się z trzech wątków: głównego wątku, który uruchamia main()metodę, wątku obliczeniowego i wątku odczytującego dane wejściowe z klawiatury. Gdy nie ma wejścia z klawiatury, blokuje się wątek odczytu. Załóżmy, że wątek odczytu ma najwyższy priorytet, a wątek obliczeniowy ma najniższy priorytet. (Dla uproszczenia załóżmy również, że żadne inne wewnętrzne wątki JVM nie są dostępne). Rysunek 1 ilustruje wykonanie tych trzech wątków.

W momencie T0 zaczyna działać główny wątek. W czasie T1 wątek główny rozpoczyna wątek obliczeniowy. Ponieważ wątek obliczeniowy ma niższy priorytet niż wątek główny, wątek obliczeniowy czeka na procesor. W momencie T2 główny wątek rozpoczyna wątek odczytu. Ponieważ wątek odczytu ma wyższy priorytet niż wątek główny, główny wątek czeka na procesor podczas pracy wątku odczytu. W czasie T3 blokuje wątek odczytu i wątek główny działa. W czasie T4 wątek odczytu odblokowuje się i działa; główny wątek czeka. Wreszcie w czasie T5 uruchamiane są bloki wątku odczytu i wątek główny. Ta przemiana w wykonywaniu między odczytem a głównym wątkiem trwa tak długo, jak działa program. Wątek obliczeniowy nigdy nie działa, ponieważ ma najniższy priorytet i dlatego nie zwraca uwagi procesora,sytuacja znana jakogłód procesora .

Możemy zmienić ten scenariusz, nadając wątkowi obliczeniowemu ten sam priorytet, co wątkowi głównemu. Rysunek 2 przedstawia wynik, począwszy od czasu T2. (Przed T2 rysunek 2 jest identyczny z rysunkiem 1.)

W czasie T2 wątek odczytujący działa, podczas gdy wątek główny i obliczeniowy czekają na procesor. W czasie T3 działają bloki wątku odczytu i wątek obliczeniowy, ponieważ wątek główny działał tuż przed wątkiem odczytującym. W czasie T4 wątek odczytu odblokowuje się i działa; wątki główne i obliczeniowe czekają. W czasie T5 działają bloki wątku odczytującego i wątek główny, ponieważ wątek obliczeniowy biegnie tuż przed wątkiem odczytującym. Ta naprzemienność wykonywania wątków głównych i obliczeniowych trwa tak długo, jak program działa i zależy od działania i blokowania wątku o wyższym priorytecie.

Musimy wziąć pod uwagę ostatnią pozycję w planowaniu zielonych wątków. Co się dzieje, gdy wątek o niższym priorytecie posiada blokadę, której wymaga wątek o wyższym priorytecie? Wątek o wyższym priorytecie blokuje się, ponieważ nie może uzyskać blokady, co oznacza, że ​​wątek o wyższym priorytecie ma faktycznie ten sam priorytet, co wątek o niższym priorytecie. Na przykład, wątek o priorytecie 6 próbuje uzyskać blokadę, którą posiada wątek o priorytecie 3. Ponieważ wątek o priorytecie 6 musi czekać, aż może uzyskać blokadę, wątek o priorytecie 6 ma priorytet 3 - zjawisko znane jako odwrócenie priorytetu .

Odwrócenie priorytetów może znacznie opóźnić wykonanie wątku o wyższym priorytecie. Na przykład załóżmy, że masz trzy wątki z priorytetami 3, 4 i 9. Wątek o priorytecie 3 jest uruchomiony, a pozostałe wątki są zablokowane. Załóżmy, że wątek o priorytecie 3 przechwytuje blokadę, a wątek o priorytecie 4 odblokowuje się. Wątek o priorytecie 4 staje się aktualnie działającym wątkiem. Ponieważ wątek o priorytecie 9 wymaga blokady, nadal czeka, aż wątek o priorytecie 3 zwolni blokadę. Jednak wątek o priorytecie 3 nie może zwolnić blokady do czasu zablokowania lub zakończenia wątku o priorytecie 4. W rezultacie wątek o priorytecie 9 opóźnia wykonanie.