Java SE, Laboratorium nr 8 Wątki Wątki w Jawie umożliwiają współbieżność operacji. Wątki umożliwiają równoległe wykonywanie instrukcji. Wątki wymagają mniej zasobów niż osobne procesy. Wynika to z tego, że wątki są wykonywane w ramach procesu - każdy proces ma przynajmniej jeden wątek. Wątki współdzielą zasoby należne do danego procesu i to jest przyczyną z jednej strony ich wydajności, a z drugiej problemów komunikacji. Każdy program w Java ma przynajmniej jeden wątek - main thread, który ma zdolność do uruchamiania dalszych wątków. Każdy wątek w Java jest powiązany z instancją klasy Thread. Są dwie strategie tworzenia aplikacji wielowątkowych: bezpośrednia kontrola i zarządzanie wątkami (powoływanie do życia obiektów wątków), "abstrakcyjna" kontrola (zdajemy się na zewnętrznego wywoływacza). Tworzenie i uruchamianie wątków Są dwa sposoby tworzenia wątków: Poprzez powołanie do życia obiektu implementującego interfejs Runnable. (Listing1) Obiekt Runnable jest przekazywany do konstruktora Thread. Interfejs Runnable posiada jedną metodę run, w której umieszcza się instrukcje, które chcemy wykonać w ramach danego wątku. Poprzez powołanie do życia klasy dziedziczącej po klasie Thread. (Listing 2) Klasa Thread implementuje interfejs Runnable, a w nim ciało metody run jest puste. W obu przypadkach musimy wykonać Thread.start w celu odpalenia nowego wątku. //Listing 1 public class HelloRunnable implements Runnable { System.out.println("Hello from a thread!"); public static void main(string args[]) { (new Thread(new HelloRunnable())).start(); (new Thread(new HelloRunnable())).start(); 1
//Listing 2 public class HelloThread extends Thread { System.out.println("Hello from a thread!"); public static void main(string args[]) { (new HelloThread()).start(); Której z powyższych metod należy używać? Pierwsza metoda, implementacja interfejsu Runnable jest bardziej ogólna. Wynika to z tego, że nasza klasa może wtedy dziedziczyć od innej klasy - nie mamy zablokowanej hierarchii dziedziczenia. Drugi sposób jest prostszy do obsługi w typowych aplikacjach. Usypianie wątków Thread.sleep jest metodą, której wykonanie skutkuje zawieszeniem wykonywania instrukcji wątku, na rzecz którego metodę wykonano. Sleep jest skutecznym sposobem zarządzania czasem procesora, który na czas uśpienia jest przez dany wątek zwalniany na rzecz pozostałych wątków danej aplikacji oraz innych aplikacji w danym systemie. Metoda sleep jest używana do nadawania określonej kolejności, rytmu instrukcjom oraz do oczekiwania na różne wydarzenia. Są 2 przeciążone deklaracje sleep: przejmująca jako argument czas w milisekundach, przyjmująca jako argument czas w nanosekundach. Nie ma gwarancji, że faktyczny czas uśpienia danego wątku będzie dokładnie taki, jak przekażemy do sleep. Tłumaczy się to uwarunkowaniami różnych OS. Dodatkowo czas uśpienia wątku może zostać przerwany (o tym później). Podsumowując, nie możemy liczyć na precyzyjne usypianie wątków. //Listing 3 public class SleepMessages { public static void main(string args[]) throws InterruptedException { String importantinfo[] = { "Ala ma kota.", "A kot ma Ale.", "Ala lubi swojego kota.", "A kot lubi Ale." ; 2
for (int i = 0;i < importantinfo.length;i++) { //Pause for 4 seconds Thread.sleep(4000); //Print a message System.out.println(importantInfo[i]); Przerywanie wątkom Przerwa, ang. interrupt, wskazuje, że wątek ma zatrzymać bieżące operacje i ewentualnie wykonać coś innego. Programista określa jak wątek reaguje na przerwanie, na ogół wątek, któremu przerwano kończy się. Żeby poprawnie obsłużyć przerwanie wątek musi go umieć obsłużyć. W jaki sposób wątek jest w stanie obsłużyć swoje własne przerwanie? Sytuacja nr 1: w metodzie sprawdzamy wyjątek InterruptedException, narzucają nam to metody, które wykonujemy. Sytuacja nr 2: metody nie wymagają obsłużenia wyjątku w.w., wtedy musimy jawnie sprawdzić stan wątku, np. for (int i = 0; i < inputs.length; i++) { heavycrunch(inputs[i]); if (Thread.interrupted()) { return; Sytuacja nr 3: Sami wyrzucamy wyjatek, który później można obsłużyć w catch: public static void main(string[] args) { if (Thread.interrupted()) { throw new InterruptedException(); Mechanizm przerwań oparty jest na mechanizmie wewnętrznej flagi, tzw. interrupt status. Wykonanie Thread.interrupt powoduje ustawienie tej flagi. Stan tej flagi możemy sprawdzić wykonując metodę isinterrupted. Aby ustawić flagę wywołujemy Thread.interrupted. Co więcej, domyślnie każda metoda, która kończy zwróceniem wyjątku InterruptedException czyści flagę interrupt status. Należy jednak pamiętać, że status ten może bardzo szybko znów ulec zmianie. Oczekiwanie na zakończenie działania innego wątku Realizuje się metodą join. Załóżmy, że czekamy na wątek o nazwie t, wtedy w bieżącym wątku wywołujemy t.join(); W efekcie bieżący wątek jest w stanie pauzy, oczekuje na zakończenie się wątku t. 3
Metoda join występuje w wersjach przeładowanych, w których programista może nakazać wątkowi oczekiwanie na zakończenie się innego wątku przez zadany czas. Podobnie jak w przypadku sleep, nie ma jednak gwarancji że podany przez nas czas będzie faktycznie zgodny z faktycznym czasem oczekiwania. Metoda join w odpowiedzi na przerwanie zwraca wyjątek InterruptedException. //Listing 4 public class SimpleThreads { // Display a message, preceded by the name of the current thread static void threadmessage(string message) { String threadname = Thread.currentThread().getName(); System.out.format("%s: %s%n", threadname, message); private static class MessageLoop implements Runnable { String importantinfo[] = {"Ala ma kota.", "Kot ma Ale.", "Ala lubi kota.", "A kot lubi Ale." ; for (int i = 0; i < importantinfo.length; i++) { // Pause for 4 seconds Thread.sleep(4000); // Print a message threadmessage(importantinfo[i]); catch (InterruptedException e) { threadmessage("i wasn't done!"); private static class NumbersLoop implements Runnable { Integer mynumbers[] = {1,5,1,86,4,6,4,1,6,43,5,1,52,6,4,3,45,61,56,56,2,56,6,5,34,2; for (int i = 0; i < mynumbers.length; i++) { // Pause for 1 seconds Thread.sleep(1000); // Print a message threadmessage(mynumbers[i].tostring()); catch (InterruptedException e) { threadmessage("i wasn't done!"); public static void main(string args[]) throws InterruptedException { threadmessage("starting MessageLoop thread"); Thread t1 = new Thread(new MessageLoop()); 4
t1.start(); threadmessage("starting Numbers Loop thread"); Thread t2 = new Thread(new NumbersLoop()); t2.start(); while (t1.isalive() t2.isalive()) { threadmessage("still waiting..."); // Wait maximum of 1 second for threads to finish. t1.join(1000); t2.join(1000); threadmessage("finally!"); Synchronizacja Współdzielenie zasobów przez wątki prowadzi do konfliktów, których po części można uniknąć stosując mechanizm synchronizacji. Pamiętać należy jednak o uważnym synchronizowaniu zasobów w celu uniknięcia zagłodzenia/zablokowania wątku. Prosty przykład (tzw. thread inference): class Counter { private int c = 0; public void increment() { c++; public void decrement() { c--; public int value() { return c; Wątek A: pobierz wartość c Wątek B: pobierz wartość c Wątek A: zwiększ c o 1; wynik to 1 Wątek B: zmniejsz c o 1; wynik to -1 Wątek A: zapisz wynik; c to 1 Wątek B: zapisz wynik; c to -1 5
W celu uniknięcia sytuacji jak powyżej mamy do dyspozycji: mechanizm synchronizacji metod, mechanizm synchronizacji instrukcji. Synchronizacja metod: public class SynchronizedCounter { private int c = 0; public synchronized void increment() { c++; public synchronized void decrement() { c--; public synchronized int value() { return c; W efekcie, nie jest możliwe, żeby dwie synchronizowane metody wykonywały się na rzecz obiektu z danej klasy równolegle. Wykonywanie się jednej z synchronizowanych metod powoduje, że pozostałe metody są zablokowane (wstrzymane) dopóki dany wątek nie zakończy działającej metody. Po zakończeniu działania metody synchronizowanej zmiany dokonane na rzecz danego obiektu są widoczne w pozostałych wątkach. Synchronizacja polega na zbudowaniu wewnętrznej struktury blokady monitora (intrinsic lock/monitor lock). Monitor umożliwia wyłączność dostępu do danego zasobu. Każdy obiekt posiada swój własny monitor. Wątek wymagający dostępu do danego obiektu musi "przejąć" monitor tego obiektu przed jego wykorzystaniem oraz zwolnić po. Mówi się o posiadaniu monitora przez dany wątek - jest to czas od przejęcia do zwolnienia monitora. Dwa wątki równolegle nie mogą posiadać tego samego monitora, wątek już posiadający monitor blokuje do niego dostęp. Wywołanie metody synchronizowanej powoduje automatyczne przejęcie obiektów z tej metody przez wątek. Zwolnienie monitora następuje: po zakończeniu metody zarówno w sposób naturalny (return) jak i poprzez nastąpienie wyjątku. Jeśli metoda jest typu static, metoda synchronizowana przejmuje monitor obiektu związanego z daną klasą: Class object. Stąd, monitor na obiekcie klasy powoduje, że nie jest możliwy np. dostęp do pól statycznych z poziomu żadnej instancji tej klasy. Synchronizacja (bloku) instrukcji: Drugim sposobem synchronizacji w Java jest synchronizacja instrukcji. Synchronizując blok konieczne jest jawne wskazanie obiektu, którego monitor chcemy przejąć, np.: 6
public void addname(string name) { synchronized(this) { lastname = name; namecount++; namelist.add(name); W powyższym przykładzie widzimy, że synchronizacją objęte są pola lastname i namecount, natomiast metody innych obiektów tu obiekt namelist nie powinny być synchronizowane. Synchronizując i przejmując monitor obiektu B z poziomu obiektu A możemy niesłusznie spowodować problem z dostępem do obiektu B z poziomu jego własnych metod. Gdyby nie można było synchronizować instrukcji, metoda powyżej musiała by zostać podzielona na 2 metody. Metoda 2 wyłącznie dodawałaby name do namelist. Synchronizacja instrukcji jest stosowana również do poprawy współbieżności programu na poziomie pojedynczych instrukcji, np.: public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; public void inc2() { synchronized(lock2) { c2++; Pamiętać należy o ostrożnym stosowaniu konstrukcji jak powyżej. Powtórna synchronizacja: wątek może ponownie przejąć monitor zajęty przez siebie samego. Dzięki temu łatwiejszej jest pisanie kodu, bowiem z poziomu synchronizowanej metody A możemy jawnie/niejawnie wywołać synchronizowaną metodę B ponownie przejmującą już zajęty przez siebie samego monitor. Gdyby nie było takiego mechanizmu bardzo łatwo by było napisać samoblokujący się kod. Dostęp atomiczny (atomic): Przez pojęcie atomiczny rozumiemy taki proces/akcję, która wykonuje się albo w całości albo wcale. Przed zakończeniem wykonywania atomicznej czynności na zewnątrz nie widać żadnych jej efektów. Wybrane czynności są/mogą być atomiczne: 7
czytanie/zapis zmiennych prymitywnych za wyjątkiem typów long i double, odczyt/zapis wszystkich zmiennych zadeklarowanych jako volatile, w tym zmiennych typu long i double. Pamiętać należy, że używanie wyłącznie słowa kluczowego volatile nie rozwiązuje wszystkich problemów współbieżnego dostępu wątków do zasobów. Niepożądane stany wątków Zakleszczenie następuje wtedy, gdy dwa lub więcej wątki zostają zablokowane na stałe w oczekiwaniu na siebie. Zagłodzenie następuje wtedy, kiedy wątek nie ma regularnego dostępu do współdzielonych zasobów i nie może ukończyć działania. Typowo zagłodzenie powstaje kiedy napiszemy algorytm szeregowania z niesprawiedliwym podziałem zasobów. Livelock następuje wtedy, kiedy dwa (lub więcej) wątków nie jest w stanie kontynuować normalne działanie wysyłając sobie równocześnie nawzajem sygnał do rozpoczęcia pracy. Bloki strzeżone; wait oraz notifyall W celu tymczasowego wstrzymania działania wątku, np. w sytuacji gdy nastąpiło jakieś zdarzenie, zaleca się pisanie bloków strzeżonych z instrukcją wait, np. public synchronized void guardedpart() { while(!cond) { wait(); catch (InterruptedException e) { System.out.println("Desired condition cond has been satisfied!"); Zaleca się stosowanie wait przy oczekiwaniu wątku na spełnienie warunku. Stosowanie wait niesie za sobą wyjątek InterruptedException. W kodzie powyżej jego ciało jest puste, ponieważ interesuje nas wyłącznie warunek cond. Uzupełnieniem metody powyżej jest metoda, w której nastąpić może zmiana zmiennej cond: public synchronized notifypart() { cond = true; notifyall(); 8
Widzimy tu metodę notifyall(), która informuje wszystkie wątki czekające na odblokowanie monitora, że nastąpiła zmiana. Dostępna jest również metoda notify(), która informuje i budzi jeden obiekt. Nie mamy jednak kontroli nad tym, który z wątków zostanie obudzony. Z tej przyczyny notify() wykorzystywany jest np. w aplikacjach przetwarzających dane, gdzie wątki współbieżnie wykonują wiele zbliżonych jakościowo zadań. //Listing 5. Zrodlo: Tutorials Oracle public class Drop { // Message sent from producer // to consumer. private String message; // True if consumer should wait // for producer to send message, // false if producer should wait for // consumer to retrieve message. private boolean empty = true; public synchronized String take() { // Wait until message is // available. while (empty) { wait(); catch (InterruptedException e) { // Toggle status. empty = true; // Notify producer that // status has changed. notifyall(); return message; public synchronized void put(string message) { // Wait until message has // been retrieved. while (!empty) { wait(); catch (InterruptedException e) { // Toggle status. empty = false; // Store message. this.message = message; // Notify consumer that status // has changed. notifyall(); 9
import java.util.random; public class Producer implements Runnable { private Drop drop; public Producer(Drop drop) { this.drop = drop; String importantinfo[] = { "Mares eat oats", "Does eat oats", "Little lambs eat ivy", "A kid will eat ivy too" ; Random random = new Random(); for (int i = 0; i < importantinfo.length; i++) { drop.put(importantinfo[i]); System.out.println("wsadzono: " + importantinfo[i]); Thread.sleep(random.nextInt(5000)); catch (InterruptedException e) { drop.put("done"); import java.util.random; public class Consumer implements Runnable { private Drop drop; public Consumer(Drop drop) { this.drop = drop; Random random = new Random(); for (String message = drop.take();!message.equals("done"); message = drop.take()) { System.out.format("MESSAGE RECEIVED: %s%n", message); Thread.sleep(random.nextInt(5000)); catch (InterruptedException e) { public class ProducerConsumerExample { public static void main(string[] args) { Drop drop = new Drop(); (new Thread(new Producer(drop))).start(); (new Thread(new Consumer(drop))).start(); 10
Wątki daemony Wątek daemon to taki wątek, który automatycznie kończy działanie wraz z zakończeniem wykonywania się wątków nie-daemonów. Typowo daemony to usługi, które działają na rzecz zwykłych wątków. W celu ustawienia wątku jako daemona wystarczy: setdaemon(true); Zadania 1. Przygotuj klasę wątku, który na konsolę wypisze k razy komunikat msg; k i msg to argumenty konstruktora. W f. main uruchom 3 takie wątki dla: a. 20, Ala ma kota. b. 10, xyzxyzxyzxyzxyzxyzxyzxyz c. 30,.. 2. Przygotuj klasę wątku, który na konsolę wypisze k razy komunikat msg, po czym zostanie uśpiony na p sekund, p, k oraz msg są argumentami konstruktora. W f. main uruchom 3 takie wątki dla: a. 2, 20, Ala ma kota. b. 3, 10, xyzxyzxyzxyzxyzxyzxyzxyz c. 1, 30,.. Uśpienie wątku umieść w pętli wypisującej komunikat. 3. Przygotuj klasę wątku-daemona, która w pętli nieskończonej na konsoli wypisywać będzie słowo jestem daemonem!!!! W f. main uruchom jeden taki wątek. Aby lepiej zobaczyć efekt działania w f. main uruchom pętlę wypisującą w konsoli funkcja main 10 razy. 4. Przygotuj klasę wątku modtablicy, która w tablicy intów zamieni najmniejszy jej element na liczbę mojaliczba, mojaliczba to argument konstruktora wątku. Po dokonaniu zamiany wątek śpi przez mojsen sekund, mojsen to drugi argument konstruktora wątku. Jeśli nie ma liczby mniejszej od mojaliczba wątek kończy działanie. Zainicjalizuj tablicę 20 intów zerami. Jest to ta tablica, którą modyfikować będą wątki. Uruchom 4 wątki dla argumentów (20, 3), (11,1), (30,1) i (15,4). Dodaj metodę wypisującą w nowej linii zawartość tablicy po każdej modyfikacji. a) przygotuj wersję, gdzie tablica intów będzie polem składowym klasy b) przygotuj wersję, gdzie tablica intów będzie synchronizowaną kolekcją, np. java.util.concurrent.blockingqueue; 11