Wątki

Koziołek

1 Wstęp
2 Tworzenie i uruchamianie wątków
3 Synchronizacja
4 Wątki Demony i grupy wątków
5 Przydatne linki

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 <font size="2" name="Courier New">Runnable</span>. Interfejs ten posiada tylko jedną metodę <font size="2" name="Courier New">run()</span>. 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 <font size="2" name="Courier New">Thread</span>:

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 <font size="2" name="Courier New">run()</span>, gdyż rozszerzamy klasę.

Tworzenie i uruchamianie wątków

Jeżeli mamy już klasę która posiada metodę <font size="2" name="Courier New">run()</span> 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 <font size="2" name="Courier New">w1</span> i <font size="2" name="Courier New">w2</span>. Pierwszy z nich to obiekt klasy implementującej interfejs <font size="2" name="Courier New">Runnable</span>, drugi to obiekt klasy dziedziczącej po <font size="2" name="Courier New">Thread</span>. Tutaj widać podstawową różnicę pomiędzy obiema metodami tworzenia klas. Zaimplementowanie interfejsu wymusza stworzenie obiektu <font size="2" name="Courier New">Thread</span>, któremu jako parametr konstruktora dajemy obiekt implementujący <font size="2" name="Courier New">Runnable</span>. Dopiero tak utworzony obiekt posiada metodę <font size="2" name="Courier New">start()</span>, 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 <font size="2" name="Courier New">Thread</span>

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 <font size="2" name="Courier New">Watek</span> i <font size="2" name="Courier New">Watek2</span> 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 <font size="2" name="Courier New">main</span> 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 <font size="2" name="Courier New">main</span>. Każda grupa może mieć podgrupy.

Przydatne linki

5 komentarzy

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. ;)

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ść.

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.

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

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.