1 Aplikacje w Javie- wykład 11 Wątki-podstawy Treści prezentowane w wykładzie zostały oparte o: Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 http://docs.oracle.com/javase/8/docs/ http://docs.oracle.com/javase/9/docs/ C. S. Horstmann, G. Cornell, Java. Podstawy, Helion, Gliwice 2013 C. S. Horstmann, Java, Techniki zaawansowane(wydanie10), Helion, Gliwice 2017
Programowanie współbieżne 2 Uruchomienie dowolnego programu (aplikacji) powoduje stworzenie procesu w systemie operacyjnym. Dla każdego procesu alokowane są przez system wymagane przez niego zasoby np. pamięciowe i plikowe. Proces - to wykonujący się program wraz z dynamicznie przydzielanymi mu przez system zasobami (np. pamięcią operacyjną, zasobami plikowymi) oraz, ewentualnie, innymi kontekstami wykonania programu (np. obiektami tworzonymi przez program). Systemy wielozadaniowe pozwalają na (teoretycznie) równoległe wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym swoje zasoby. W systemach wielozadaniowych i wielowątkowych - jednostką wykonawczą procesu jest wątek. Każdy proces ma co najmniej jeden działający wątek, ale może też mieć ich wiele. Proces "posiada" zasoby i inne konteksty, wykonaniem "zadań" procesu zajmują się wątki (swego rodzaju podprocesy, wykonujące różne działania w kontekście jednego procesu). Wątki działają (teoretycznie) równolegle. Zatem równoległość działań w ramach procesu (jednego programu) osiągamy przez uruchamianie kilku różnych wątków. Wątek - to sekwencja działań, która może wykonywać się równolegle z innymi sekwencjami działań w kontekście danego procesu (programu).
Programowanie współbieżne 4 Proces wykonuje się poprzez wykonanie jego wątków. Zatem, zwolnienie procesora przez wątek jednego procesu i przydzielenie procesora wątkowi innego procesu wymaga "przeładowania" kontekstu procesu, bowiem każdy proces ma swój niezależny kontekst (np. przestrzeń adresową, odniesienia do otwartych plików). Podstawowa różnica pomiędzy procesami i wątkami polega na tym, że różne wątki w ramach jednego procesu mają dostęp do całego kontekstu tego procesu (m.in. przydzielonych mu zasobów). Wobec tego zamiana wątków jednego procesu "przy procesorze" jest wykonywana szybciej niż zamiana procesów (wątków różnych procesów). Z punktu widzenia programisty wspólny dostęp wszystkich wątków jednego procesu do kontekstu tego procesu ma zarówno zalety jak i wady. Zaletą jest możliwość łatwego dostępu do wspólnych danych programu. Wadą - brak ochrony danych programu przed równoległymi zmianami, dokonywanymi przez różne wątki, co może prowadzić do niespójności danych, a czego unikanie wiąże się z koniecznością synchronizacji działania wątków.
Tworzenie i uruchamianie wątku (klasa Thread) Uruchamianiem i zarządzaniem wątkami w Javie zajmuje się klasa Thread. Aby uruchomić wątek należy stworzyć obiekt klasy Thread i użyć metody start() wobec tego obiektu. Kod, wykonujący się w wątku (sekwencja działań, wykonująca się równolegle z innymi działaniami programu) określany jest przez obiekt klasy implementującej interfejs Runnable. Interfejs ten zawiera deklarację metody run(), w której przy implementacji zapisujemy kod. Ten kod będzie wykonywany w wątku (równolegle z innymi fragmentami wykonującymi się w innych wątkach). Metoda run() określa co ma robić wątek. Klasa Thread implementuje interfejs Runnable (podając "pustą" metodę run). Pierwszy sposób tworzenia i uruchamiania wątku 1) Zdefiniować własną klasę dziedziczącą Thread (np. class Timer extends Thread) 2) Przedefiniować odziedziczoną metodą run(), podając w niej działania, które ma wykonywać wątek 3) Stworzyć obiekt naszej klasy (np. Timer timer = new Timer(...); 4) Wysłać mu komunikat start() (np. timer.start()) 5
Tworzenie i uruchamianie wątku (klasa Thread) 6 Niech klasa Timer służy do zliczania czasu (nie mylić tej klasy z klasami Timer z pakietu javax.swing oraz Timer z pakietu java.util; tutaj pod tą nazwą opisujemy własną klasę - licznik czasu). public class Timer extends Thread { public void run() { int time = 0; //licznik sekund while (true) { try { this.sleep(1000); //usypiamy wątek na 1 sekundę catch(interruptedexception exc) { System.out.println("Wątek zliczania czasu zoostał przerwany."); return; time++; int minutes = time/60; int sec = time%60; System.out.println(minutes + ":" + sec);
Tworzenie i uruchamianie wątku (klasa Thread) 7 Kodu zliczającego upływ czasu dostarczyliśmy w metodzie run(). Zmienna time jest licznikiem sekund. W pętli while usypiamy wątek, wykonujący tę metodę run(), na 1000 milisekund, czyli 1 sekundę (statyczna metoda sleep z klasy Thread, która usypia bieżący wątek), po czym zwiększamy licznik sekund (zmienna time) i wyprowadzamy informację o upływie czasu na konsolę. W trakcie uśpienia wątek jest odsuwany od procesora (kod metody run nie wykonuje się wtedy). Dzięki temu uzyskujemy pożądany efekt. Metoda sleep może zgłaszać wyjątek InterruptedException, który powstaje na skutek przerwania działania wątku (metodą interrupt()). Dlatego musimy obsługiwać ten wyjątek. Przykład. Licznik czasu możemy zastosować np. w programie, który wymaga od użytkownika podania wszystkich stolic (lądowych) sąsiadów Polski. Wraz z podawaniem kolejnych stolic chcemy wypisywać na konsoli informację o upływającym czasie.
Tworzenie i uruchamianie wątku (klasa Thread) 8 import java.util.*; import static javax.swing.joptionpane.*; public class Quiz { public static void main(string args[]) { // Stolice do odgadnięcia Set<String> cap = new HashSet<>( Arrays.asList("praga", "bratysława","moskwa", "berlin", "kijów", "wilno", "mińsk")); //stolice już podane Set<String> entered = new HashSet<>(); showmessagedialog(null, "Podaj stolice lądowych sąsiadów Polski"); String askmsg = "Wpisz kolejną stolicę:" ; int count = 0; // ile podano prawidłowych odpowiedzi // Uruchomienie wątku zliczającego //i pokazującego upływający czas new Timer().start();
Tworzenie i uruchamianie wątku (klasa Thread) 9 // dopóki nie podano wszystkich stolic for (int n=cap.size(); count<n;) { String in = JOptionPane.showInputDialog("Odpowiedzi:" + count + '/'+ n +'\n' +askmsg); if (in == null) break; in = in.tolowercase(); // jeśli tej stolicy wcześniej nie podano // i odp. jest prawidłowa if (!entered.contains(in) && cap.contains(in)){ count++; entered.add(in);
Tworzenie i uruchamianie wątku (klasa Thread) 10 W pętli pobieramy dane od użytkownika, dopóki nie wpisze on prawidłowo wszystkich znajdujących się w zbiorze cap stolic lub nie przerwie wpisywania, wybierając w dialogu Cancel. Ponieważ wcześniej utworzyliśmy i uruchomiliśmy wątek zliczania czasu: Timer tm = new Timer(); tm.start(); lub zwięźlej: new Timer().start(); to nasz program równolegle wykonuje dwa zadania: interakcję z użytkownikiem (pytania o stolice) oraz wypisywanie na konsoli informacji o upływającym czasie. Wywołanie metody start() na rzecz obiektu klasy Thread powoduje uruchomienie metody run() z klasy Timer. To wywołanie nie blokuje głównego programu; wykonywane są dalsze jego instrukcje, a kod metody run() działa równolegle.
Tworzenie i uruchamianie wątku (interfejs Runnable) 11 Jak już wiemy, kod wykonywany przez wątek podajemy w metodzie run(). A metoda run() może być zdefiniowana w dowolnej klasie implementującej interfejs Runnable. Klasa Thread dostarcza zaś konstruktora, którego argument jest typu Runnnable. Konstruktor ten tworzy wątek, który będzie wykonywał kod zapisany w metodzie run() w klasie obiektu, do którego referencję przekazano wspomnianemu wyżej konstruktorowi. Drugi sposób tworzenia i uruchamiania wątków. 1) Zdefiniować klasę implementującą interfejs Runnable (np. class X implements Runnable). 2) Dostarczyć w niej definicji metody run() (co ma robić wątek). 3) Utworzyć obiekt tej klasy (np. X x = new X(); ) 4) Utworzyć obiekt klasy Thread, przekazując w konstruktorze referencję do obiektu utworzonego w p.3 (np.thread thread = new Thread(x);). 5) Wywołać na rzecz nowoutworzonego obiektu klasy Thread metodę start ( thread.start();)
Tworzenie i uruchamianie wątku (interfejs Runnable) 12 Oprogramowanie uprzednio omawianego licznika czasu przy użyciu drugiego sposobu: class Timer implements Runnable { public void run() { int time = 0; while (true) { try { Thread.sleep(1000); catch(interruptedexception exc) { System.out.println("Wątek zliczania czasu został przerwany."); return; time++; int minutes = time/60; int sec = time%60; System.out.println(minutes + ":" + sec);
Tworzenie i uruchamianie wątku (interfejs Runnable) Wówczas utworzenie i uruchomienie wątku zliczającego czas (w innej klasie): Timer tm = new Timer(); Thread thread = new Thread(tm); thread.start(); lub zwięźlej: new Thread(new Timer()).start(); Jak poprzednio, w metodzie run() dla uśpienia wątku na sekundę używamy statycznej metody sleep z klasy Thread, która usypia bieżący wątek (w tym przypadku ten, w którym wykonuje się ta metoda run()). Nie mogliśmy tym razem napisać this.sleep(), bo nowa klasa Timer nie dziedziczy już klasy Thread. Drugi sposób tworzenia i uruchamiania wątków ma pewne zalety w stosunku do korzystania wyłącznie z klasy Thread (czyli sposobu pierwszego): niekiedy daje lepsze możliwości separowania kodu (kod odpowiedzialny za pracę wątku może być wyraźnie wyodrębniony w klasie implementującej Runnable). w niektórych okolicznościach - mianowicie, gdy chcemy umieścić metodę run() w klasie, która dziedziczy jakąś inną klasę - jest jedynym możliwym sposobem. 13