16 listopada 2008

SCJP - Tworzenie i uruchamianie wątków

Czasem spotyka się z opinią, że programowanie w języku Java jest na tyle proste, że studia informatyczne są zupełnie nie potrzebne by móc to robić dobrze – wystarczy kurs w stylu „Java dla opornych”. Oczywiście wszystko zależy od tego jak zdefiniujemy słowo „dobrze”. Można argumentować że aplikacja – by działać – nie musi być dobrze zaprojektowana, tj. z uwzględnieniem paradygmatów obiektowości i wzorców projektowych, że nie trzeba rozumieć pojęcia złożoności obliczeniowej żeby implementować algorytmy; ale nie da się zaprzeczyć temu, że aby tworzyć bezpieczne aplikacje współbieżne trzeba znać podstawowe zagadnienia programowania współbieżnego. Tu już nie chodzi przecież ani o elegancję ani nawet o wydajności. Tu chodzi o poprawność. W dzisiejszym artykule napiszę o podstawach pracy z wątkami w języku Java czyli właściwie o tym, jak sprawić by problemy współbieżności zaczęły się pojawiać – tj. jak uruchomić kod w osobnym wątku wykonania.

Wątek to nic innego jak pewien kod, który wykonywany jest przez Wirtualną Maszynę Javy niezależnie od kodu innych wątków, przy czym owo niezależnie oznacza współbieżnie. Aby wykonać pewien kod w osobnym wątku wykonania należy przede wszystkim określić jaki kod chcemy wykonać; należy więc nowy wątek zdefiniować. Definicja wątku to nic innego jak implementacja podklasy klasy java.lang.Thread.

Implementacja programu w języku Java jest równoznaczna z implementacją metody main(…). Uruchomienie programu napisanego w Javie to de facto uruchomienie metody main(…). Zakończenie się metody main(…) jest równoznaczne z zakończeniem się programu. Metoda main(…) to nic innego jak implementacja głównego wątku wykonania naszej aplikacji – wątek ten uruchamiamy uruchamiając aplikację. Każdy inny wątek implementujemy tworząc podklasę klasy java.lang.Thread i nadpisując jej metodę run() a więc analogicznie – implementując metodę, tyle że run() a nie main(…). Definicja wątku to jednak nic więcej jak tylko pewna klasa zawierająca fragment kodu w metodzie run(). Aby utworzyć nowy wątek i uruchomić w jego ramach kod zdefiniowany we wspomnianej metodzie run() należy wywołać metodę start() na instancji tejże klasy – dopiero wówczas tworzony i równocześnie uruchamiany jest wątek. Przykład poniżej.

public class TestClass {
public static void main(String[] args) {
System.out.println("Kod wykonany w wątku głównym");

Thread newThread = new MyThread();

// dopiero wywołując metodę start() tworzymy nowy wątek
newThread.start();
}
}

// ta klasa to tylko definicja wątku
class MyThread extends Thread {
public void run() {
System.out.println("Kod wykonany w nowym wątku");
}
}

Definiowanie wątków poprzez implementację podklas klasy Thread jest jak najbardziej poprawne, jednak zalecane jest aby robić to nieco inaczej. Metoda start() z klasy Thread pokazana w powyższym przykładzie tworzy nowy wątek i uruchamia w jego ramach kod zdefiniowany w metodzie run(). Oczywiście kod ten może nie robić nic innego, jak tylko uruchamiać kolejną metodę zdefiniowaną w innej klasie, w szczególności metodę run() zdefiniowaną w klasie implementującej interfejs java.lang.Runnable. Klasa Thread udostępnia także konstruktor jednoargumentowy, akceptujący instancje klasy implementującej interfejs Runnable. Implementacja metody run() w klasie Thread jest właśnie taka, że jeśli tworząc jej instancję przekazaliśmy obiekt Runnable, to w ramach wątku zostanie uruchomiona metoda run() tego właśnie obiektu. Wszystko powinno stać się jasne po przestudiowaniu kolejnego przykładu.

public class TestClass {
public static void main(String[] args) {
System.out.println("Kod wykonany w wątku głównym");

// wątek tworzymy na bazie instancji klasy implementującej Runnable
Thread newThread = new Thread(new MyRunnable());

newThread.start();
}
}

class MyRunnable implements Runnable {
public void run() {
System.out.println("Kod wykonany w nowym wątku");
}
}

Oprócz metody start() klasa Thread udostępnia jeszcze szereg innych, mniej lub bardziej kluczowych metod. Jedną z nich jest metoda getName(), która zwraca nazwę wątku. Wątek główny zawsze nazywa się ‘main’. Wątki tworzone przez programistę mają nazwy wygenerowane automatycznie przez Wirtualną Maszynę Javy, ale naturalnie nazwa ta może być zmieniona. W tym celu można użyć metody setName(…) albo konstruktora, który jako argument wywołania (drugi jeśli przekazujemy instancję Runnable lub pierwszy i jedyny, jeśli nie) akceptuje nazwę nowo tworzonego wątku. Nazwy te nie muszą być unikalne – różne wątki mogą mieć tą samą nazwę. Unikalny natomiast jest identyfikator wątku, który możemy odczytać z użyciem metody getId() i którego nie możemy zmienić. Przydatna jest także statyczna metoda currentThread(), która zwraca instancję klasy Thread opisującą aktualnie wykonywany wątek.

Ważną właściwością wątku jest jego stan. Aktualny stan wątku możemy sprawdzić posługując się metodą getState(), która zwraca jedną z wartości wyliczeniowych typu java.lang.Thread.State. Nowo utworzonej instancji wątku, który jeszcze nie został uruchomiony odpowiada stan NEW. Gdy wątek jest startowany, jego stan automatycznie zmienia się na RUNNABLE, by w końcu – potencjalnie przechodząc przez różne stany przejściowe – zakończyć się w stanie TERMINATED. Bardzo ważne jest by zapamiętać – zwłaszcza z perspektywy egzaminu SCJP – że wystartować można tylko i wyłącznie wątek który znajduje się w stanie NEW. Oznacza to, że każdy wątek może być uruchomiony tylko raz! Każde następne – tj. nie pierwsze – wywołanie metody start() na instancji reprezentującej wątek spowoduje zwrócenie wyjątku IllegalThreadStateException. Zerknijmy na poniższy przykład.

public class TestClass {
public static void main(String[] args) {
Thread newThread = new Thread(new MyRunnable(), "newThread");

System.out.println(
"Wątek " + newThread.getName() + " w stanie " + newThread.getState());

newThread.start();
}
}

class MyRunnable implements Runnable {
public void run() {
Thread thread = Thread.currentThread();

System.out.println(
"Wątek " + thread.getName() + " w stanie " + thread.getState());
}
}

Zauważmy, że w metodzie run() implementowanej w klasie MyRunnable nie mamy dostępu do instancji klasy Thread – bo też póki co kod ten nie jest powiązany z żadną instancją reprezentującą wątek – a więc musimy się posłużyć metodą statyczną currentThread(). Uruchomienie tego programu spowoduje wyświetlenie napisu:

Wątek newThread w stanie NEW
Wątek newThread w stanie RUNNABLE

Wiemy już, że wątek możemy uruchomić tylko i wyłącznie raz (także wątek który się zakończył nie może być uruchomiony ponownie), ale czy możemy utworzyć i uruchomić wiele wątków wykonujących jednocześnie (współbieżnie) ten sam kod? Oczywiście możemy – przykład poniżej.

public class TestClass {
public static void main(String[] args) {
Runnable runnable = new MyRunnable();

// wszystkie wątki będą wykonywały dokładnie ten sam kod
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);

threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}

class MyRunnable implements Runnable {
private int val = 0;

public void run() {
for (int i = 0; i < 100; i++)
System.out.println(
"id == " + Thread.currentThread().getId() + ", val == " + getVal());
}

private synchronized int getVal() {
return ++val;
}
}

Powyższy program tworzy i uruchamia cztery nowe wątki, z których każdy, współbieżnie z pozostałymi wykonuje metodę run() dokładnie tej samej instancji klasy MyRunnable, a więc wyświetla i inkrementuje wartość tej samej zmiennej ‘val’. Poniżej fragment tego, co wypisał ten program po uruchomieniu w trakcie moich testów. Jak widać, wątki rzeczywiście przeplatają się między sobą (choć często występują też długie fragmenty, gdy nieprzerwanie wykonywał się jeden z wątków). Co ciekawe, w drugiej linii pojawia się wartość 150, mimo że w linii pierwszej pojawiła się wartość 224. Dowodzi to, że wątek o identyfikatorze 9 wykonał operację getVal() w momencie gdy wartość zmiennej ‘val’ wynosiła 149 poczym przestał się wykonywać przed wypisaniem komunikatu „id == 9, val == 150” i wznowił swe wykonanie, tj. wypisał ten komunikat, dopiero po tym jak inne wątki zwiększyły wartość tej zmiennej grubo ponad 200.

id == 10, val == 224
id == 9, val == 150
id == 10, val == 226
id == 11, val == 225
id == 10, val == 228

2 komentarze:

Adaslaw pisze...

Zdanie:
Zakończenie się metody main(…) jest równoznaczne z zakończeniem się programu.

... chyba nie jest prawdziwe.

Powinno ono brzmieć:
All threads that are not daemon threads have died, either by returning from the call to the run method or by throwing an exception that propagates beyond the run method.

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Thread.html

Mariusz Lipiński pisze...

Tak, zgadza się. Wywód rozpoczęty zacytowanym przez ciebie zdaniem miał za zadanie wyjaśnić, że implementacja kodu który ma się uruchomić w osobnym wątku sprowadza się do implementacji metody run(), analogicznie jak implementacja głównego wątku to implementacja metody main(). Czasem wygodnie jest posłużyć się pewnym uproszczeniem, żeby wytłumaczyć pewne podstawowe zagadnienia. Próba powiedzenia wszystkiego od razu mogłaby się zakończyć zbytnim zamieszaniem tematu i w rezultacie nikt nic by nie zrozumiał.

Pozdrawiam,
Mariusz Lipiński