Wątki w Javie
Jednym z powodów, dla którego język Java stał tak popularny w latach 90-tych była prostota z jaką programista mógł zaprojektować środowisko wielowątkowe. W tamtym okresie nie było zbyt wiele technologii, które mogłyby się liczyć z językiem stworzonym przez firmę Sun Microsystems. Musisz wiedzieć, że początkowo Java była językiem dedykowanym jako wsparcie dla pierwszej przeglądarki internetowej Netscape i pewnie dlatego po dziś dzień, mylona jest przez laików z JavaScriptem.
Jednym z głównym powodów było to, że pierwsze przeglądarki były bardzo prostymi programami do parsowania plików HTML (które w końcu są przecież tekstami ze specjalnymi znacznikami). Zwykły html nie dawał jednak możliwości jakiejkolwiek zaawansowanej interakcji, np. poprzez tworzenie prostych gier, czatów, itp. Twórcy Javy mieli na to odpowiedź w postaci prostych programów, które można było umieścić na stronie www, nazywanych apletami*. Dziś nikt już ich nie używa, a Java nie jest wspierana przez żadną z przeglądarek, jednak wielowątkowość zaprojektowana przez jej autorów, wciąż jest bardzo często używana.
Czym są wątki?
Wątek można rozumieć, jako fragment kodu, który równolegle wykonuje swoją pracę w trakcie działania danego programu. Nie każdy wątek musi być zaprojektowany w języku Java, dlatego powyższa definicja jest bardzo ogólna. W omawianym przypadku skupiam się tylko na wątkach kontrolowanych przez wirtualną maszynę Java (JVM).
Na początek pokażę Ci dwa sposoby w jaki można zaimplementować własny wątek w Javie. We wszystkich przykładach używam Loggera, z racji, że jest on synchronizowany.
Rozszerzanie klasy Thread
public class CustomThread extends Thread{ private static final Logger LOGGER = Logger.getLogger(CustomThread.class.getName()); @Override public void run() { LOGGER.info("Thread " + Thread.currentThread().getName() + " was started"); } }
W przypadku mojej klasy CustomThread jedyne co musiałem zrobić to nadpisać metodę run. Kod w niej zawarty to instrukcje, które będą wykonywane w każdy wątku z osobna.
public class CustomRunnable implements Runnable{ private static final Logger LOGGER = Logger.getLogger(CustomRunnable.class.getName()); @Override public void run() { LOGGER.info("Thread " + Thread.currentThread().getName() + " was started"); } }
Zaimplementowanie interfejsu Runnable
Inną możliwość jest po prostu użycie interfejsu Runnable. Podobnie jak w przypadku pierwszym, instrukcje swojego wątku powinny znaleźć się w metodzie run. Główną różnicą między oboma przykładami jest sposób ich użycia. W przypadku skorzystania z dziedziczenia po Thread wystarczy gotowy wątek odpalić po prostu za pomocą metody start. Metoda ta w swoim wnętrzu odpala napisaną przeze mnie metodę run. Bardziej skomplikowane jest użycie klasy implementującej Runnable, bowiem tutaj należy jeszcze opakować ją w klasę Thread.
public static void main(String[] args) { CustomThread thread = new CustomThread(); thread.setName("first"); thread.start(); CustomRunnable runnable = new CustomRunnable(); Thread runnableThread = new Thread(runnable, "second"); runnableThread.start(); }
Na konsoli wyświetlą się oba napisy:
lut 28, 2021 8:06:31 PM advanced.lesson9.CustomThread run INFO: Thread first was started lut 28, 2021 8:06:31 PM advanced.lesson9.CustomRunnable run INFO: Thread second was started
Zadasz pewnie pytanie, co się dzieje, gdy odpalisz metodę run z klasy CustomRunnable, zamiast użyć start na obiekcie runnableThread. Otóż na pierwszy rzut oka, nie będziesz widzieć różnicy. Niestety jest ona wyraźna, bowiem odpalenie metody run bezpośrednio nie spowoduje realizacji Twojego kodu w nowym wątku. Zamiast tego Java wykona ten kod a pomocą obecnie używanego wątku. Także w praktyce, możesz równie dobrze po prostu wypisać napis w metodzie main. Dlatego pamiętaj, aby zawsze startować wątki za pomocą metody start a nie run.
Dodatkowo możesz ponazywać swoje wątki zarówno korzystając z setera setName, jak i z parametru konstruktora klasy Thread. Jeśli potrzebujesz odnieść się do obecnie wykonywanego wątku, sięgnij po metodę currentThread.
Fazy życia każdego wątku
Jak widzisz wątki, możesz traktować jak niezależne podprogramy w Javie. W zależności jaką operację chcesz na nich wykonać mogą być one w innym stanie.
- New – stan początkowy. Oznacza wątek, który jeszcze nie został uruchomiony.
- Runnable – metoda start została użyta na wątku. JVM przetwarza jego zawartość.
- Blocked – wątek nie ma dostępu do sekcji krytycznej**. Będzie kontynuować swoją pracę w momencie, kiedy inne wątki zakończą na tym samym zasobie swoją pracę.
- Waiting – stan bardzo podobny do powyższego. Drobną różnicą w stosunku do poprzedniej opcji jest to, że nie sprawdza dostępność jakiegoś zasobu, lecz oczekuje na sygnał z innego wątku, aktualnie działającego, na jego zakończenie.
- Timed Waiting – pozostaje w stanie oczekiwania aż do momentu, gdy upłynie określony okres czasu.
- Terminated – określa wątek, który zakończył swoją pracę lub jego aktywność została zawieszona przez niespodziewane wydarzenie (np. wyjątek lub błąd programu).
Usypianie wątku
Oprócz startowania wątku, bardzo częstym zachowaniem jest tymczasowe zamrażanie jego działanie na jakiś czas. W języku Java do wykonania tej operacji służą dwie metody: wait oraz sleep. Pierwsza działa na obiekcie (nie na wątku) i ustawia wątek pracujący na nim w stanie oczekiwania (waiting), aż do momentu gdy nie zostanie na tym samym obiekcie wywołana metoda notify lub notifyAll. W szczegółach będę omawiał działanie tych metod w lekcji o synchronizacji wątków. W przeciwieństwie do wait, metoda sleep wykonywana jest na aktualnie pracującym wątku (jest to metoda statyczna klasy Thread). Wyrzuca ona wyjątek InterruptedException, wynika to z prostego faktu, że jedynym sposobem, aby obudzić wątek, który śpi jest użycie metody interrupt.
public class CustomThread extends Thread{ private static final Logger LOGGER = Logger.getLogger(CustomThread.class.getName()); @Override public void run() { LOGGER.info("Thread " + Thread.currentThread().getName() + " was started"); try { Thread.sleep(10000); } catch (InterruptedException e) { LOGGER.info("Thread " + Thread.currentThread().getName() + " was stopped by exception or error."); } LOGGER.info("Thread " + Thread.currentThread().getName() + " finished his work."); } }
Warto pamiętać, że czas zamrożenia wątku przy użyciu metody sleep, jest wyrażony w milisekundach a nie w sekundach.
Zatrzymywanie wątku
Kolejną częstą operacją jest wymuszenie zakończenia działania wątku. Można porównać to do zabijania procesów w systemie operacyjnym (choć wątki to nie to samo co procesy). Istnieje kilka sposobów, które mogą do tego posłużyć, jednak używanie każdej z nich niesie ze sobą pewne konsekwencje. W tym przykładzie omówię działanie dwóch z nich, metody stop i oraz interrupt.
Spróbuję najpierw zatrzymać drugi z uruchamianych wątków z pomocą metody stop:
CustomThread thread = new CustomThread(); thread.setName("first"); thread.start(); CustomRunnable runnable = new CustomRunnable(); Thread runnableThread = new Thread(runnable, "second"); runnableThread.start(); runnableThread.stop();
Efekt na konsoli?
lut 28, 2021 8:11:36 PM advanced.lesson9.CustomThread run INFO: Thread first was started lut 28, 2021 8:11:46 PM advanced.lesson9.CustomThread run INFO: Thread first finished his work.
Wygląda jakby wszystko zadziałało prawidłowo. Teraz odpalę ten sam kod tylko z ostatnią linią:
thread.stop();
Na konsoli pusto. Teoretyczne powinien zostać zastopowany pierwszą wątek, w praktyce nic się nie zadziało. Jeśli zadasz pytanie dlaczego, to odpowiem, że nie wiem. Co więcej, nie ma pojęcia czy na Twoim komputerze kompilator zachowa się tak samo! Oficjalne wytłumaczenie pochodzące z samego kodu Javy, brzmi:
„If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior.”
Wniosek jest bardzo prosty, nigdy nie używaj tej metody. 🙂 Została ona zostawiona w kodzie tylko i wyłącznie dla osiągnięcia kompatybilności wstecz.
Wątki zatrzymywane za pomocą interrupt.
Zmodyfikuję tutaj mój kod odrobinę, aby ułatwić zauważenie rezultatu.
CustomThread thread = new CustomThread(); thread.setName("first"); thread.start(); CustomThread threadTwo = new CustomThread(); threadTwo.setName("second"); threadTwo.start(); thread.interrupt();
Rezultat na konsoli:
lut 28, 2021 8:31:08 PM advanced.lesson9.CustomThread run INFO: Thread first was started lut 28, 2021 8:31:08 PM advanced.lesson9.CustomThread run INFO: Thread second was started lut 28, 2021 8:31:08 PM advanced.lesson9.CustomThread run INFO: Thread first was stopped by exception or error. lut 28, 2021 8:31:08 PM advanced.lesson9.CustomThread run INFO: Thread first finished his work. lut 28, 2021 8:31:18 PM advanced.lesson9.CustomThread run INFO: Thread second finished his work.
Wszystko tu działa tak jak powinno. Bez względu czy chcę zatrzymać pierwszy czy drugi wątek, zostaje on poprawnie zakończony, co widać po logu, znajdującym się w klauzuli catch.
Klasa Thread posiada jeszcze wiele innych metod, jednak nie będę już ich opisywał w tej i tak długiej lekcji. W następnym wpisie postaram się przybliżyć Ci działanie barier w Javie.
*Aplety – https://www.oracle.com/java/technologies/applets.html
**Sekcja krytyczna – https://www.javatpoint.com/os-critical-section-problem
Kod z lekcji: https://github.com/developeronthego/java-advanced/tree/master/src/main/java/advanced/lesson9
Przypomnienie o tym, jak korzystać z loggerów: Java #39: logowanie aplikacji