Wątki
Spis treści
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 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");
}
}
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");
}
}
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();
}
}
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");
}
}
}
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");
}
}
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.



A co do artykułu krótki acz treściwy jak na taka długość.
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.