Threads #1

Create and use the Thread class and the Runnable interface.

Chcąc podejść do egzaminu 1Z0-804 musimy wykazać się znajomością mechanizmu wątków. W pierwszym artykule z tego rozdziału omówimy sposoby tworzenia nowych wątków.


Na skróty:

Thread [Opis] [API]
Runnable [Opis] [API]

Repozytorium z przykładami [GitHub]


Pierwszy wątek masz już raczej za sobą

Jeżeli kiedykolwiek przyszło Ci napisać i uruchomić, chociażby najprostszy program w Javie, to można powiedzieć, że masz już doświadczenie w tworzeniu wątków.

W każdym programie główna pętla aplikacji rozpoczyna się od wywołania metody main. Metoda ta wołana jest w utworzonym specjalnie dla niej wątku.

package com.javathehat.threads1.main;

public class MainThread {

  public static void main(String[] args) {
    
    System.out.println("HelloWorld");
    
    String threadName = Thread.currentThread().getName();
    
    System.out.println("Thread name = " + threadName);
  }

}

Powyżej znajduje się kompletny kod aplikacji, składającej się z jednej klasy. Aplikacja wypisuje na konsolę dwie linijki tekstu. Pierwsza to zwykły ciąg znaków „HelloWorld”. W drugiej wypisywana jest nazwa wątku, w którym wykonywany jest ten kod. Uruchomienie aplikacji skutkuje wypisaniem na konsolę tekstu:

HelloWorld
Thread name = main

Na tworzenie wątku main nie mamy zbyt wielkiego wpływu, ale istotne jest, żeby o nim pamiętać – na egzaminie może pojawić się pytanie o to, ile wątków zostało utworzonych w trakcie wykonywania danego kodu. Nie zapomnij doliczyć głównego wątku.

Przejdźmy teraz do omówienia sposobów tworzenia i uruchamiania swoich własnych wątków.

Rozszerzenie klasy Thread

Najprostszym sposobem, aby utworzyć klasę, z której da się zainicjować wątek, jest dziedziczenie po klasie Thread.

public class SampleThread extends Thread{}

Przykładowo tworzymy klasę SampleThread, która rozszerza klasę Thread. Teoretycznie tyle wystarczy, ale przecież chcielibyśmy, aby nasz wątek robił coś konkretnego. W tym celu musimy nadpisać (ang. override) metodę run(). Kod tej metody wykona się gdy wystartujemy nasz wątek.

public class SampleThread extends Thread{
  
  public void run(){
    
    for(int i = 1; i <=3;  i++){
      System.out.println(i);
    }
    System.out.println(Thread.currentThread().getName());
  }

}

W powyższym przykładzie zakodowaliśmy klasę wątku, który po uruchomieniu wypisze on na konsolę liczby: 1,2,3, oraz swoją nazwę. Aby uruchomić nasz nowy wątek, wystarczy utworzyć nowy obiekt klasy SampleThread i wywołać na nim metodę start(). Zróbmy to w oddzielnej klasie:

public class SampleThreadTest {
  
  public static void main(String[] args) {
    
    SampleThread sampleThread1 = new SampleThread();
    SampleThread sampleThread2 = new SampleThread();
    
    sampleThread1.start();
    sampleThread2.start();
  }

}

Utworzyliśmy tu dwa obiekty klasy SampleThread i uruchomiliśmy dwa nowe wątki, wywołując na tych obiektach metodę start().

Ponieważ oba wątki współdzielą jedną konsolę, nie mamy kontroli nad tym, w jakiej kolejności będą wypisywać tekst. Pewne jest tylko, że każdy wątek zawsze przed wypisaniem swojej nazwy, wypisze kolejno liczby od 1 do 3. Dla kolejnych uruchomień tego samego kodu, możemy mieć różne wyniki, przykładowo:

1
2
3
1
2
3
Thread-1
Thread-0

lub:

1
2
3
Thread-0
1
2
3
Thread-1

Zapamiętaj. Jeżeli kilka wątków ma możliwość pracy (jest w statusie runnable, ale o tym później), to nie ma sposobu, żeby nakazać maszynie Javy przekazanie kontroli temu konkretnemu wątkowi, któremu byśmy sobie życzyli. Można jedynie dać maszynie sugestię, którym wątkiem powinna się zająć, ale ostatecznie decyzja i tak jest dla nas nieprzewidywalna.

To wszystko. Podsumowując, aby uruchomić swój własny wątek tym sposobem, musimy:

  1. Utworzyć klasę dziedziczącą po klasie Thread.
  2. Nadpisać metodę run().
  3. Utworzyć obiekt naszej klasy.
  4. Wywołać na tym obiekcie metodę start().

Sposób ten, ma jednak jedną podstawową wadę, którą możemy wyeliminować korzystając ze sposobu numer dwa.

Implementacja interfejsu Runnable

Ponieważ w Javie nie ma wielodziedziczenia, utworzenie wątku pierwszym sposobem nie zawsze będzie dla nas korzystne. Jeśli rozszerzymy klasę Thread, nie będziemy mogli rozszerzyć już żadnej innej klasy. A co jeśli musimy?

Wtedy z pomocą przychodzi interfejs Runnable. Implementując ten interfejs w klasie, nie musimy już rozszerzać klasy Thread. Interfejs Runnable posiada tylko jedną metodę – run(), spełnia ona takie same zadanie jak metoda run() klasy Thread.

Zakodujmy zatem klasę, która nie rozszerza klasy Thread, ale będziemy mogli utworzyć z niej nowy wątek:

public class SampleRunnable implements Runnable {

  @Override
  public void run() {
    System.out.println("1");
    System.out.println("2");
    System.out.println(Thread.currentThread().getName());
  }

}

Kiedy wykorzystując klasę SampleRunnable utworzymy i uruchomimy nowy wątek, na konsoli zostanie wypisane:

1
2
Thread-0

Dobra, tylko jak uruchomić ten wątek, skoro obiekt klasy SampleRunnable nie dziedziczy po Thread i nie ma metody start()?

Musimy utworzyć obiekt klasy Thread, przekazując w konstruktorze obiekt naszej klasy implementującej Runnable.

SampleRunnable sampleRunnable1 = new SampleRunnable(); 
Thread thread1 = new Thread(sampleRunnable1);

Dalej, postępujemy już jak ze standardowym obiektem klasy Thread. Zobaczmy więc cały przykład, który analogicznie jak w opisie pierwszego sposobu tworzenia wątków, inicjuje i uruchamia dwa nowe wątki:

public class SampleRunnableTest {

  public static void main(String[] args) {
    
    SampleRunnable sampleRunnable1 = new SampleRunnable();
    SampleRunnable sampleRunnable2 = new SampleRunnable();
    
    Thread thread1 = new Thread(sampleRunnable1);
    Thread thread2 = new Thread(sampleRunnable2);
    
    thread1.start();
    thread2.start();
  }
  
}

Podobnie jak wcześniej, dwa przykładowe, możliwe wyniki:

1
2
Thread-1
1
2
Thread-0

lub:

1
2
1
2
Thread-0
Thread-1

Podsumowując, aby utworzyć nowy wątek za pomocą interfejsu Runnable:

  1. Tworzymy klasę implementującą interfejs Runnable.
  2. Nadpisujemy metodę run().
  3. Inicjujemy obiekt utworzonej klasy.
  4. Inicjujemy obiekt klasy Thread, podając w konstruktorze obiekt naszej utworzonej klasy.
  5. Na obiekcie klasy Thread wywołujemy metodę start().

Coś więcej o klasie Thread

Inicjalizacja obiektu wątku, a jego uruchomienie

Thread ma cztery konstruktory:

Thread()
Thread(String name);
Thread(Runnable target);
Thread(Runnable target, String name)

Z ich pomocą możemy utworzyć sam obiekt Thread, lub opakować nim obiekt implementujący interfejs Runnable. Dodatkowo możemy nadać wątkowi swoją własną nazwę jeśli nie chcemy, aby pozostała domyślna.

Samo utworzenie obiektu klasy Thread nie tworzy jeszcze nowego wątku!

Również, jawne wywołanie metody run() nie tworzy nowego wątku.

Jeśli uruchomimy poniższy program, który korzysta z utworzonej wcześniej klasy SampleThread:

public class ThreadTests {
    
    public static void main(String[] args) {
        
        SampleThread thread1 = new SampleThread();
        thread1.run(); //Nie tworzy nowego wątku
        
        SampleThread thread2 = new SampleThread();
        thread2.run(); //Nie tworzy nowego wątku
    }

}

Wynik zawsze będzie ten sam:

1
2
3
main
1
2
3
main

Jedynym wątkiem utworzonym podczas wykonywania programu będzie wątek „main” . Tylko metoda start() tworzy nowy wątek. Co prowadzi nas do następnej bardzo ważnej informacji:

Jeśli rozszerzając klasę Thread nadpiszemy metodę start() , obiekt naszej klasy straci możliwość tworzenia nowych wątków.

Aby temu zapobiec, w nadpisanej metodzie, musimy wywołać start() z klasy nadrzędnej, czyli super.start().

Stwórzmy klasę, która rozszerza Thread i nadpisuje metodę start(), bez wywołania super.start(). Obiekt tej klasy w metodzie run() wypisuje do konsoli nazwę swojego wątku:

public class SampleBadThread extends Thread {
    
    public void run(){
        System.out.println("Watek: " + Thread.currentThread().getName());
    }
    
    public void start(){
        run();
    }

}

I spróbujmy utworzyć nowy wątek:

SampleBadThread sampleBadThread = new SampleBadThread();
sampleBadThread.start();

Teoretycznie utworzyliśmy klasę rozszerzającą klasę Thread i nadpisaliśmy metodę run(). Powinno dać nam to możliwość tworzenia nowych wątków. Jednak, dodatkowo nadpisaliśmy metodę start(), co w efekcie daje taki wynik na konsoli:

Watek: main

Ciało metody run() wykonało się głównym wątku. Tego typu pułapka jest wręcz pewniakiem na egzaminie.


Podanie wątku w konstruktorze wątku

Kolejnym faktem, na który warto zwrócić uwagę jest, to, że obiekt klasy rozszerzającej Thread, jest jednocześnie obiektem Runnable, czyli możemy go przekazać w konstruktorze klasy Thread.

Rozważmy taki kod:

SampleThread sampleThread = new SampleThread();
Thread thread = new Thread(sampleThread);
    
thread.start();

Mamy naszą klasę SampleThread, która rozszerza klasę Thread. Tworzymy jej obiekt i podajemy go w konstruktorze tworząc obiekt klasy Thread, a następnie na tym obiekcie wołamy metodę start(). Pytanie brzmi, która metoda start() i która metoda run() się wykona? Te z obiektu klasy SampleThread czy Thread? A może i z tego i z tego?

Wykona się tylko jedna metoda start() i jedna metoda run() – z obiektu, który przekazaliśmy w konstruktorze, czyli w naszym przypadku z obiektu klasy SampleThread.


Priorytet wątku

Jak już wcześniej wspomniałem, możemy zasugerować maszynie Javy, któremu z wielu dostępnych wątków dać dostęp do procesora. Jednym ze sposobów jest ustawienie priorytetu wątku. Służy do tego metoda:

setPriority(int newPriority)

W argumencie podajemy int z zakresu od 1 do 10. Gdzie 10 to najwyższy priorytet.

Domyślnie wątek dostaje taki sam priorytet, jaki miał nadrzędny wątek, w którym został stworzony.

Jednak nie ma gwarancji, że maszyna Javy zawsze wybierze wątek o najwyższym priorytecie. Może, ale nie musi…


Jeszcze jeden przykład na koniec

Na koniec przeanalizujmy sobie kompletny przypadek wykorzystania wątków. Tym razem zorganizujemy wyścigi.

public class Driver extends Thread{
  
  private final int laps = 3;
  private String name;
  
  public Driver(String name){
    this.name = name;
  }
  
  public void run(){
    race();
  }
  
  private void race(){
    
    for(int i = 1 ; i <= laps; i++){
      System.out.println(String.format("Kierowca: %s, okrozenie: %d", name,i));
    }
    
    System.out.println(String.format("%s ukonczyl wyscig!", name));
    
  }
  

  public static void main(String[] args) {
    
    System.out.println("Kierowcy na start!");
    
    Driver d1 = new Driver("Waldemar");
    Driver d2 = new Driver("Henryk");
    Driver d3 = new Driver("Ksawery");
    
    System.out.println("START!");
    
    d1.start();
    d2.start();
    d3.start();

  }

}

Ponieważ to tylko przykład, pozwoliłem sobie zamieścić cały kod w jednej klasie. Tworzymy tutaj klasę Driver, która dziedziczy po klasie Thread. W konstruktorze, nadajemy naszemu kierowcy imię. Klasa ma również zdefiniowaną stałą mówiącą o tym, ile okrążeń liczy sobie wyścig. W metodzie run() nakazujemy naszemu kierowcy pokonać kolejno wszystkie okrążenia wyścigu.

W metodzie main() tworzymy sobie trzech kierowców i po kolei każdemu dajemy sygnał do startu.

Oto jeden z możliwych wyników wykonania kodu:

Kierowcy na start!
START!
Kierowca: Ksawery, okrozenie: 1
Kierowca: Henryk, okrozenie: 1
Kierowca: Waldemar, okrozenie: 1
Kierowca: Ksawery, okrozenie: 2
Kierowca: Henryk, okrozenie: 2
Kierowca: Ksawery, okrozenie: 3
Kierowca: Waldemar, okrozenie: 2
Ksawery ukonczyl wyscig!
Kierowca: Henryk, okrozenie: 3
Kierowca: Waldemar, okrozenie: 3
Henryk ukonczyl wyscig!
Waldemar ukonczyl wyscig!

Zauważ, że mimo tego, że wątki startujemy w oddzielnych instrukcjach, jeden po drugim, to wcale nie przeważa na wyniku „wyścigu” . Akurat w tym przypadku, wątek uruchomiony jako pierwszy, zakończył się jako ostatni.

W następnym artykule z tego rozdziału dokładnie omówimy cykl życia wątku.