Inspiracja , wiedza , realizacja
Jsystems

W przebudowie

Login



Java

Oracle

Linux

Android

PostgreSQL

Microsoft SQL Server

Sterowanie wątkami

Kamil Perczyński
Data dodania: Oct 30, 2015
Data aktualizacji: Oct 30, 2015

Pisząc programy wielowątkowe bardzo często musimy zapewnić nie tylko synchronizację wątków (wykonywanie jakiegoś kodu tylko przez jeden wątek naraz), ale też wykonanie operacji we właściwej kolejności. Albo wykonanie części operacji przez jeden wątek, potem części operacji przez inny, potem znów wykonanie części pierwszego wątku itd. Albo wykonanie części jakiegoś wątku dopiero po ustabilizowaniu danych przez inny. Albo...

Do tego wszystkiego proste synchronized nie wystarczy. Musimy zacząć sterować wątkami i sprawić aby zaczęły się między sobą komunikować. Służą do tego trzy metody z klasy Object - wait(), notify(), notifyAll().

Za ich pomocą możemy wykonać dowolnie skomplikowany układ koordynacji wątków. Ich używanie jest niestety dość nieintuicyjne m.in. dlatego, że są to metody bardzo niskopoziomowe, a w przypadku błędów otrzymamy wyjątek IllegalMonitorStateException, który tak naprawdę nie zawiera żadnych konkretnych informacji.

Na początek ogólnie wyjaśnię działanie tych metod. Szczegóły wyjdą już w kodzie.

Użycie każdej z nich musi nastąpić w bloku zsynchronizowanym!

Napiszemy teraz program, który uruchomi dwa wątki (oba będą wykonywać jakieś proste operacje w pętlach), ale schemat wykonania będzie następujący: Wątek1, Wątek2, Wątek1, Wątek2, Wątek1.... Będzie to implementacja podstawowego schematu producent/konsument.

Zaczniemy od napisania klas Producenta i Konsumenta, które później będziemy modyfikować, tak aby uzyskać pożądany efekt.

Stała s1 jest nam potrzebna tylko po to aby móc zsynchronizować oba wątki na tym samym obiekcie. Skomunikujemy teraz nasze wątki:

Metoda wait() może podnosić wyjątek InterruptedException, gdy wątek, który czeka został poproszony o przerwanie wykonania ( wywołano na nim interrupt() );

Przy takim układzie (wydawałoby się, że prawidłowym), Consumer czeka, aż Producer wywoła notify(), następnie wypisuje na ekran powiadomienie i znów czeka na powiadomienie od Producer'a. Niestety przy uruchomieniu (przez JVM, a nie przez nas :)) wątku Consumer kod producenta wykona się 10 razy, a konsumenta tylko raz. Jeśli JVM uruchomi wątek producenta jako pierwszy - kod konsumenta nie wykona się nigdy. Przyczyną jest niski poziom metod wait() i notify(). Producent wykona swoje ciało zanim konsument wykona wait(). Z kolei 10 wywołań notify() nie zostanie odłożonych w pamięci. Dlatego gdy konsument wywołuje wait() wątek producenta już umarł.

Dlatego musimy wprowadzić komunikację dwustronną. Producent musi wykonać część swojego ciała, następnie powiadomić konsumenta, konsument wykona część swojego ciała i powiadomi czekającego producenta.

Wywołanie wait( int timeout ) powoduje wstrzymanie wykonania wątku maksymalnie na czas podany jako argument (w milisekundach). Taki wait() jest potrzebny konsumentowi tylko dla pierwszego obrotu pętli.

Konsument jako kolejka blokująca

Zaimplementujemy producenta i konsumenta z blokowaniem wątków, współdzielonym zasobem będzie ArrayDeque. Jeden wątek (albo kilka) będzie asynchronicznie dodawał do kolejki elementy, a drugi będzie je asynchronicznie wypisywał na ekran, ale jeśli kolejka będzie pusta, to wątek poczeka na nowy wpis.

Zaczniemy oczywiście od synchronizacji dwóch wątków na jednej kolejce, ale jeszcze bez sterowania.

Uruchomienie spowoduje (na 99.9%) wyjątek NoSuchElementException podniesiony przez metodę remove() kolejki.

Jak widać wykonanie ciała wątku konsumenta nastąpiło przed dodaniem wszystkich elementów. Aby rozwiązać ten problem, wystarczy, że w bloku zsynchronizowanym konsumenta dodamy instrukcję wait() gdy kolejka będzie pusta, a każdy Adder powiadomi konsumenta o dodaniu nowego elementu.

Tak to wygląda. Dodałem losowe opóźnienia w dodawaniu elementów do kolejki, aby móc zaobserwować blokowanie wąteku konsumenta. W zasadzie wątpliwości może budzić tylko umieszczenie wait() w pętli. Jest to dość ważne. W tym konkretnym przykładzie zastosowanie if'a, i while'a da ten sam wynik. Ale gdybyśmy mieli kilkunastu niezależnie działających konsumentów mogłaby zdażyć się sytuacja w której konsument wszedłby w wait(), został powiadomiony, ale zasób, który miał skonsumować został mu zabrany przez inny wątek. W praktyce wait() zawsze powinien być wywoływany w pętli.

Pamiętaj! W warunkach wysokiej konkurencji, powiadomienie wątku nie oznacza, że może on bez żadnych przeszkód wykonać swoje operacje.

Jeszcze tylko output z wykonia. Zwróć uwagę na czas dodania elementów do kolejki: