Java

Wątki



Wstęp


Wątek (proces lekki) jest to, najogólniej, podstawowa jednostka wykorzystania procesora. Wątek może działać tylko w obszarze jednego procesu. Potocznie jest rozumiany jako coś mniejszego niż proces.
W języku Java wątki można rozumieć na takiej samej zasadzie jak procesy. Wątki działają zazwyczaj w jednym kontekście aplikacji w ramach danej Maszyny Wirtualnej (VM). Współdzielą one między sobą:
  • stertę VM. W tym obiekty static i singletony
  • otwarte pliki
Każdy wątek posiada za to własny stos.
Najprostszą metodą na stworzenie nowego wątku jest zaimplementowanie interfejsu Runnable. Interfejs ten posiada tylko jedną metodę run(). Przykładowa implementacja:
public class Watek implements Runnable {
    public void run() {
        System.out.println("Jestem sobie zwykłym Wątkiem implementującym interfejs Runnable");        
    }
}

Inną metodą jest rozszerzenie klasy  Thread:
public class Watek2 extends Thread {
    public void run() {
        System.out.println("Jestem sobie zwykłym Wątkiem rozszerzającym klasę Thread");
    }
}

Tutaj należy zwrócić uwagę na to że nie ma potrzeby pisania własnej metody run(), gdyż rozszerzamy klasę.

Tworzenie i uruchamianie wątków


Jeżeli mamy już klasę która posiada metodę run() zobaczmy jak należy z niej korzystać.
public class Uruchom {
 
    public static void main( String[] args ) {
        Watek w1 = new Watek();
        Watek2 w2 = new Watek2();
 
        (new Thread(w1)).start();
        w2.start();
 
    }
}

Na początek tworzymy dwa wątki w1 i w2. Pierwszy z nich to obiekt klasy implementującej interfejs Runnable, drugi to obiekt klasy dziedziczącej po Thread. Tutaj widać podstawową różnicę pomiędzy obiema metodami tworzenia klas. Zaimplementowanie interfejsu wymusza stworzenie obiektu Thread, któremu jako parametr konstruktora dajemy obiekt implementujący Runnable. Dopiero tak utworzony obiekt posiada metodę start(), która uruchamia wątek. Drugie podejście powoduje iż nie musimy tworzyć dodatkowego obiektu.
Które podejście jest lepsze? Niewątpliwie pierwsze ponieważ:
  • opiera się na interfejsach co jest znacznie bardziej elastyczną metodą niż dziedziczenie
  • nie zaburza hierarchii klas poprzez dziedziczenie po Thread

Synchronizacja


Tworząc wątki należy pamiętać, że współdzielą pomiędzy sobą pamięć i zasoby. Może to prowadzić do kolizji, a te do błędów. Błędy spowodowane przez kolizje jest bardzo ciężko znaleźć i usunąć.  W celu uniknięcia kolizji Java udostępnia mechanizm synchronizacji wątków. Zanim jednak omówimy synchronizację należy przyjrzeć się dlaczego jest ona ważna.
Wątki mają przydzielony pewien czas procesora. Po wyczerpaniu się czasu procesora VM wywłaszcza wątek zapisując jego stan (licznik rozkazów, stan rejestrów) i przekazuje procesor innemu wątkowi. Zmodyfikowanie klas Watek i Watek2 oraz uruchomienie programu zobrazuje ten mechanizm w praktyce:
//zmodyfikowane klasy Watek i Watek2
public class Watek
    implements Runnable {
    public void run() {
        for(int i = 0; i<100000; i++){
            System.out.println("Jestem sobie zwykłym Wątkiem implementującym interfejs Runnable");
        }
    }
}
/******/
/******/
public class Watek2
    extends Thread {
    public void run() {
        for(int i = 0; i<100000; i++){
            System.out.println("Jestem sobie zwykłym Wątkiem rozszerzającym klasę Thread");
        }
    }
}

Po uruchomieniu programu zobaczymy iż przez pewien czas wypisywany jest pierwszy napis, potem drugi, a następnie znowu pierwszy. Jak widać wątki wykonywane są naprzemiennie a ilość czasu przydzielonego przez VM jest losowa (lecz nie mniejsza niż pewna wartość zależna od konkretnej implementacji VM). Takie zachowanie może, jak już pisałem, prowadzić do błędów. Przykładem takiego błędu może być zakłamanie wartości zmiennej.
Niech wątek T1:
  • pobiera zmienną a
  • zwiększa jej wartość o 1
  • zapisuje
Niech wątek T2:
  • pobiera zmienną a
  • zmniejsza jej wartość o 1
  • zapisuje
Program główny uruchamia wątki T1 i T2 w niekończonej pętli. Niech a = 5.
Co się może stać?
Wątek T1 i T2 pobierają zmienną w tym samym momencie. Zmienna trafia na lokalny stos wątku. Następnie wątki T1 i T2 wykonują na lokalnych kopiach operacje i w tym momencie:
  • Dla T1 a = 6
  • Dla T2 a = 4
Wątki zapisują nową wartość zmiennej. Zmienna ma teraz wartość 4 lub 6 (zależy od kolejności w jakiej wątki zapisały zmienną) co jest wynikiem nieprawidłowym. Prawidłowa wartość to 5 ponieważ dodano 1 i odjęto 1. Uniknięcie tego problemu jest stosunkowo proste jeżeli zastosuje się mechanizm synchronizacji. Składnia polecenia wygląda w następujący sposób:
    synchronized ( mutex ) {
 
    }

gdzie mutex to obiekt który chcemy by był synchronizowany. Możemy też metodę oznaczyć jako synchronizowaną:
public static synchronized void metoda(){}

Jeżeli metoda lub blok kodu jest synchronizowany to dostęp do niego ma tylko wątek który wywołał tą metodę jako pierwszy. Inne wątki muszą czekać aż pierwszy wątek zakończy wykonanie danej metody. Poniższy kod pokazuje jak to działa:
public class Uruchom {
 
    public static void main( String[] args ) {
        Watek w1 = new Watek();
        Watek2 w2 = new Watek2();
 
        (new Thread(w1)).start();
        w2.start();
    }
 
    public static synchronized void wypisz(String tekst){
        for(int i = 0; i<10000; i++)
            System.out.println(i+": "+tekst);
    }
}
/******/
/******/
public class Watek
    implements Runnable {
    public void run() {
        Uruchom.wypisz("Jestem sobie zwykłym Wątkiem implementującym interfejs Runnable");
    }
}
/******/
/******/
public class Watek2
    extends Thread {
    public void run() {
        Uruchom.wypisz("Jestem sobie zwykłym Wątkiem rozszerzającym klasę Thread");
    }
}

Po uruchomieniu programu nie zaobserwujemy już wymieszania się tekstów pochodzących z różnych wątków.

Wątki Demony i grupy wątków


Specyficznym rodzajem wątków są wątki demony. Wątki takie można rozumieć jako "rodziców" dla innych wątków. Przykładem takiego wątku może być wątek main który jest tworzony w momencie uruchomienia programu. Demony są wykorzystywane do prowadzenia serwisów dostępnych dla innych wątków. Jeżeli w systemie pozostały już tylko wątki demony to VM kończy działanie programu ponieważ nie istnieja już wątki dla których demony mogą prowadzić serwis.
Grupy wątków pozwalają na zarządzanie kilkoma watkami tak jak by były jednym obiektem. Domyślnie wszystkie wątki należą do grupy  main. Każda grupa może mieć podgrupy.

Przydatne linki


5 komentarzy

Olamagato 2012-06-10 22:52

To się powinno raczej nazywać: "Nieaktualny wstęp do wielowątkowości w Javie". Grupy wątków, to od dawna trup projektowy. Nazywanie "wątkiem" klasy implementującej Runnable, to co najmniej nadużycie, a w każdym razie niemal pewniak jako źródło błędów. Samo bezpośrednie używanie klas Thread jest w czasach od Javy 5 mniej więcej na takim poziomie abstrakcji jak używanie setjmp() w C++.
Oczywiście - ktoś powie, że łatwo krytykować zamiast samemu coś napisać. To prawda. Ale temat wątków jest tak obszerny, że same podstawy, to kilka stron maszynopisu nawet w najbardziej oszczędnej formie. Dlatego przewidywany ogrom pisaniny na razie zwyczajnie mnie przeraża. ;)

revcorey 2008-09-16 21:36

Smarze jakiś artykuł na temat wielozbierzności w javie na razie mam łącznie z przykładowymi(nie cała strona) kodami  jakieś 3 strony w writerze(docelowo może z 10). Zobaczymy co to będzie(tyle że jakość tego opracowania taka sobie). Myślę że za parę dni opublikuje.
A co do artykułu krótki acz treściwy jak na taka długość.

eloar 2008-04-30 14:19

Uważam, że przydałoby się nieco więcej na temat metod klasy Thread. Już przy prostych zadaniach przydają się różne metody tej klasy. Na przykład metoda yield(), jeśli w programie działa więcej niż jeden wątek o tym samym priorytecie.

Koziołek 2007-10-23 20:37

Da się załatwić ale nie w tym tygodniu. Jestem zawalony innymi projektami. Przypomnij się za kilka dni na pw.

RJM 2007-10-23 20:24

Właśnie się uczę tego działu. Trochę mi tu brakuje. Przydałby się przykład programu
w którym jest zastosowana synchronizacja zmiennej.

Jak dla mnie za krótki, chociaż w bardzo przystępny sposób tłumaczy wątki.
Właściwie to powinien się nazywać: "Wstęp do wielowątkowości  w Java ", bo omawia tylko podstawowe zagadnienia.
Stąd moja prośba do autora o rozwinięcie artykułu - jest niezły.

Pozdrawiam.