Java zaawansowane #9: przetwarzanie wielowątkowe

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 wyją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");
	}
}

Drugi to 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ątek, możesz traktować jak niezależny podprogram w Javie. W zależności jaką operację chcesz na nim wykonać może być on 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 inny wątek zakończy 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. Istnieje 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.

Opcja druga, czyli 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

Może Ci się również spodoba

1 Odpowiedź

  1. Tom95 pisze:

    Fajny post. Duzo mi tozjasnil. Czekam na posty odnosnie javy 8

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *