4. Wprowadzenie do  programowania współbieżnego w Javie


4.1. Podstawowe pojęcia: procesy i wątki


Proces - to wykonujący się program wraz z dynamicznie przydzielanymi mu przez system zasobami (np. pamięcią operacyjną, zasobami plikowymi) oraz, ewentualnie, innymi kontekstami wykonania programu (np. obiektami tworzonymi przez program) .


Systemy wielozadaniowe pozwalają na (praktycznie) równoległe wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym:  swoje zasoby.

W systemach wielozadaniowych i wielowątkowych  -  jednostką wykonawczą procesu jest wątek.
Każdy proces ma co najmniej jeden wykonujący się wątek, ale może też mieć ich wiele.
Proces "posiada" zasoby i inne konteksty, wykonaniem "zadań" procesu zajmują się wątki (swego rodzaju podprocesy, wykonujące różne działania w kontekście jednego procesu).

Wątki działają (praktycznie) równolegle.
Zatem równoległość działań w ramach procesu (jednego programu) osiągamy przez uruchamianie kilku różnych wątków.

Wątek - to sekwencja działań, która może wykonywać się równolegle  z innymi sekwencjami działań w kontekście danego procesu (programu).


Dodatkowych wątków w ramach jednego programu używa się często (choć nie zawsze) do wykonania czynności, które zajmują sporo czasu i przez to mogą blokować działanie całego programu. Gdy zostaną one wyodrębnione jako wątki, działanie aplikacji nie będzie wstrzymywane, a czasochłonne czynności wykonywane są (praktycznie) równolegle z innymi (może podstawowymi) działaniami aplikacji, niejako w tle.

Co tak  naprtawdę oznacza równoległość wykonywania wątków?
Przecież w każdym momencie czasu procesor może wykonywać tylko jakąś jedną instrukcję.
Zatem w systemach jednoprocesorowych tak naprawdę w każdym momencie czasu wykonuje się tylko jeden wątek i nie ma tu "prawdziwej" równoległości.
Wrażenie rownoległości dzialania wątków osiągane jest przez mechanizm przydzielania czasu procesora poszczególnym wykonującym się wątkom. Każdy wątek uzyskuje dostęp do procesora na krótki czas (kwant czasu), po czym "oddaje procesor" innemu wątkowi. Zmiany są tak szybkie, że powstaje wrażenie równoleglości działania.

Ponieważ Java jest językiem wieloplatformowym, a różne systemy operacyjne stosują różne mechanizmy udostępniania wątkom czasu procesora, pisząc programy wielowątkowe w Javie powinniśmy zakladać, że mogą one działac zarówno w środowisku "współpracy", jak i "konkurencji" (mechanizm wywłaszczania)
Zmiany wątków "u procesora" mogą dokonywać się wedle dwóch mechanizmów:

Proces wykonuje się poprzez wykonanie jego wątków. Zatem, zwolnienie procesora przez wątek jednego procesu i przydzielenie procesora wątkowi innego procesu wymaga  "przeładowania" kontekstu procesu, bowiem każdy proces ma swój niezależny kontekst (np. przestrzeń adresową, odniesienia do otwartych plików).

Podstawowa róznica pomiędzy procesami i wątkami polega na tym, że różne wątki w ramach jednego procesu mają dostęp do całego kontekstu tego procesu (m.in. przydzielonych mu zasobów).
Wobec tego zamiana wątków jednego procesu "przy procesorze" jest wykonywana szybciej niż zamiana procesów (wątków różnych procesów).

Z punktu widzenia programisty wspólny dostęp wszystkich wątków jednego procesu do kontekstu tego procesu ma zarówno zalety jak i wady.
Zaletą jest możliwość łatwego dostępu do wspólnych danych programu. Wadą - brak ochrony danych programu przed równoległymi zmianami, dokonywanymi przez różne wątki, co może prowadzić do niespójności danych, a czego unikanie wiąże się z koniecznością synchronizacji dzialania wątków.


4.2. Jak stworzyć i uruchomić nowy wątek?

Uruchamianiem wątków i zarządzaniem nimi zajmuje się klasa Thread.


Aby uruchomić wątek należy stworzyć obiekt klasy Thread i  użyć metody start() wobec tego obiektu.


Ale kod, wykonujący się jako wątek (sekwencja działań, wykonująca się równolegle z innymi działaniami programu) określany jest przez obiekt klasy implementującej interfejs Runnable.
Interfejs ten zawiera deklarację metody run(), która przy implementacji musi być zdefiniowana.
Właśnie w metodzie run() zapisujemy kod, który będzie wykonywany jako wątek (równolegle z innymi wątkami programu).

   Metoda run() określa co ma robić wątek.


Klasa Thread implementuje interfejs Runnable (podając "pustą" metodę run).

Stąd pierwszy sposób tworzenia i uruchamiania wątku.

Pierwszy sposób tworzenia i uruchamiania wątku
  1. Zdefiniować własną klasę dziedziczącą Thread (np. class Timer extends Thread)
  2. Przedefiniować odziedziczoną metodą run(), podając w niej działania, które ma wykonywać wątek
  3. Stworzyć obiekt naszej klasy (np. Timer timer = new Timer(...);
  4. Wysłać mu komunikat start() (np. timer.start()) 

Niech klasa Timer służy do zliczania czasu. Może wyglądać tak.

public class Timer extends Thread {

   public void run() {
     int time = 0;
     while (true) {
       try {
         this.sleep(1000);
       } catch(InterruptedException exc) {
           System.out.println("Wątek zliczania czasu zoostał przerwany.");
           return;
       }
       time++;
       int minutes = time/60;
       int sec = time%60;
       System.out.println(minutes + ":" + sec);
     }
   }
}
Metoda sleep może sygnalizować wyjątek InterruptedException, który powstaje na skutek przerwania działania wątku (np. przez zniecierpliowanego długim czekaniem na jego obudzenie użytkownika). Dlatego musimy obsługiwać ten wyjątek.

Licznik czasu możemy zastosować np. w następującym programie, który wymaga od użytkownika podania wszystkich stolic (lądowych) sąsiadów Polski.

import javax.swing.*;

public class Quiz {

  // Stolice do odgadnięcia
  private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
                                "Berlin", "Kijów", "Wilno", "Mińsk" };

  // Czy stolica była już podana ?
  private boolean[] entered = new boolean[CAP.length];

  public Quiz() {

    int n = CAP.length;

    JOptionPane.showMessageDialog(null, "Podaj stolice lądowych sąsiadów Polski");
    String askMsg = "Wpisz kolejną stolicę:" ;

    int count = 0;  // ile podano prawidłowych odpowiedzi

    // Uruchomienie wątku zliczającego i pokazującego upływający czas
    Timer tm = new Timer();
    tm.start();

    while (count < CAP.length) { // dopóki nie podano wszystkich stolic
      String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
                                                  '\n' + askMsg);
      if (input == null) break;
      if (isOk(input)) count++;  // jeżeli ta odpowiedź prawidłowa
    }
    System.exit(0);
  }

  // Czy odpowiedź jest prawidłowa i czy jej wcześniej nie podano?

  private boolean isOk(String s) {
    for (int i=0; i < CAP.length; i++) {
      if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
         return (entered[i] = true);
    }
    return false;
  }

  public static void main(String args[]) {
    new Quiz();
  }
}
Nasz program równolegle wykonuje dwa zadania: interakcję z użytkownikiem (pytania o stolice) oraz wypisywanie na konsoli informacji o upływającym czasie.


Oczywiście,  nic nie stoi na przeszkodzie, by odziedziczyć klasę Thread w klasie Quiz (pytającej użytkownika o stolice). Przy okazji zobaczymy, że dwa wątki mogą odwoływać się do wspólnych danych.

import javax.swing.*;

public class Quiz1a extends Thread {

  private final String[] CAP = {"Praga", "Bratysława", "Moskwa",
                                "Berlin", "Kijów", "Wilno", "Mińsk" };

  private boolean[] entered = new boolean[CAP.length];

  private int time = 0; // licznik czasu

  public Quiz1a() {
    int n = CAP.length;

    JOptionPane.showMessageDialog(null, "Podaj stolice sąsiadujących krajów");
    String askMsg = "Wpisz kolejną stolicę:" ;
    int count = 0;

    // Uruchomienie wątku zliczania czasu
    start();

    while (count < CAP.length) {
      String input = JOptionPane.showInputDialog("Odpowiedzi: " + count + '/' + n +
                                                 ".   Czas: " + getTime() + '\n' +
                                                 askMsg);
      if (input == null) break;
      if (isOk(input)) count++;
    }
    JOptionPane.showMessageDialog(null, "Czas wpisywania: " + getTime());
    System.exit(0);
  }

  // Kod który wykonuje się w odrębnym wątku
  public void run() {
    while (true) {
      try {
        this.sleep(1000);
      } catch(InterruptedException exc) {
          System.out.println("Wątek zliczania czasu został przerwany.");
          return;
      }
      time++;
      System.out.println(getTime());
    }
  }

  // Metoda zwracająca bieżący czas w formie min : sek
  private String getTime() {
    int minutes = time/60;
    int sec = time%60;
    return minutes + ":" + sec;
  }

  private boolean isOk(String s) {
    for (int i=0; i < CAP.length; i++) {
      if (s.equalsIgnoreCase(CAP[i]) && !entered[i])
         return (entered[i] = true);
    }
    return false;
  }


  public static void main(String args[]) {
    new Quiz1a();
  }
}

Problemy z dostępme do tej samej zmiennej!

Jak już wiemy, kod wykonywany przez wątek podajemy w metodzie run(). A metoda run() może być zdefiniowana w dowolnej klasie implementującej interfejs Runnable.
Klasa Thread dostarcza zaś konstruktora, którego argument jest  typu Runnnable.
Konstruktor ten  tworzy wątek, który będzie wykonywał kod zapisany w metodzie run() w klasie obiektu, do którego referencję przekazano wspomnianemu wyżej konstruktorowi.

Stąd drugi sposób tworzenia i uruchamiania wątków.


Drugi sposób tworzenia i uruchamiania wątku
  1. Zdefiniować klasę implementującą interfejs Runnable (np. class X implements Runnable).
  2. Dostarczyć w niej definicji metody run (co ma robić wątek).
  3. Utworzyć obiekt tej klasy (np.  X x = new X(); )
  4.  Utworzyć obiekt klasy Thread, przekazując w konstruktorze referencję do obiektu utworzonego w p.3 (np.Thread thread = new Thread(x);).
  5. Wywołać na rzecz nowoutworzonego obiektu klasy Thread  metodę start ( thread.start();)



Oprogramowanie uprzednio omawianego licznika czasu przy użyciu drugiego sposobu tworzenia i uruchamiania watków  wyglądałoby tak:

class Timer implements Runnable {
  public void run() {
    int time = 0;
    while (true) {
      try {
        Thread.sleep(1000);
      } catch(InterruptedException exc) {
          System.out.println("Wątek zliczania czasu zoostał przerwany.");
          return;
      }
      time++;
      int minutes = time/60;
      int sec = time%60;
      System.out.println(minutes + ":" + sec);
    }
  }
}
a utworzenie i uruchomienie wątku zliczającego czas (w innej klasie) w następujący sposób:
    Timer timer = new Timer();
    Thread thread = new Thread(timer);
    thread.start();
lub zwięźlej:

new Thread(new Timer()).start();

Drugi sposób tworzenia i uruchamiania wątków ma pewne zalety w stosunku do korzystania wyłącznie z klasy Thread (czyli omówionego wcześniej sposobu pierwszego):
Wyobraźmy sobie oto, że w znanym nam już przykładzie z samochodami (zużycie paliwa w czasie jazdy - zob. klasa Car i materiał o klasach wewnętrznych w poprzednim wykładzie) nie dysponujemy możliwością uzycia klasy Timer z pakietu javax.swing.
Sami musimy oprogramować symulację zużycia paliwa.
Jak przedtem, umówimy się, że w każdej sekundzie czasu programu zużywany jest 1 litr paliwa.

Klasa Car dziedziczy klasę Vehicle, zatem nie możemy już odziedziczyć klasy Thread. Ale możemy implementować interfejs Runnable i w klasie Car dostarczyć metody run(), która będzie symulować zużycie paliwa.

public class Car extends Vehicle implements Runnable {

    private String nrRej;
    private int tankCapacity;   // pojemność baku
    private int fuel;           // ile jest paliwa?

    public Car(String nr, Person owner, int w, int h, int l,
               int weight, int tankCap)  {
        super(owner, w, h, l, weight);
        nrRej = nr;
        tankCapacity = tankCap;
    }

    // Napełnianie baku
    public void fill(int amount)  {
      fuel += amount;
      if (fuel > tankCapacity) fuel = tankCapacity;
    }

    // Start samochodu
    public void start()  {
      if (fuel > 0)   {
          super.start();
          new Thread(this).start();  // uruchamiamy wątek zużycia paliwa
      }
      else System.out.println("Brak paliwa");
    }

    // Zatrzymanie samochodu
    public void stop()  {
        super.stop();
    }

    // Kod, który wykonuje się w odrębnym wątku
    // co 1 sek. czasu programu zużywany jest 1 litr paliwa
    public void run()  {
      while(true) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        fuel--;
        System.out.println("Paliwo: " + fuel);  // śledzimy ile jest paliwa
        if (fuel <= 0) break;    // jeżeli brak paliwa...
      }
     System.out.println("Zatrzymanie samochodu z powodu braku paliwa");
     stop();                     // zatrzymanie samochodu, bo brak paliwa
    }

    public String toString()  {
       return "Samochód nr rej " + nrRej + " - " + getState(getState());
    }
}

W klasie TestCar1 przetestujemy działanie programu na przykładzie jednego samochodu:

public class TestCar1 {

   // Symulacja upływu czasu...
   static void delay(int sek) {
     while(sek-- > 0) {
       try {
         Thread.sleep(1000);
       } catch (Exception exc)  { }
     }
    }

 public static void main(String[] args)  {
    Car c = new Car("WA1090", new Person("Janek", "0909090"),
                     100, 100, 100, 100, 50);

    c.fill(10);   // napełniamy bak
    c.start();    // ruszamy ...
    System.out.println(c + ""); // co się dzieje z samochodem
    delay(12);    // niech upłynie 12 sek. jazdy od tego momentu
    System.out.println(c + ""); // co się dzieje z samochodem
 }
}
Wyniki dzialania programu przedstawia wydruk.

Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Samochód nr rej WA1090 - STOI


Wydaje się, że wszystko jest w porządku. Ale co by się stało gdyby po upływie 3 sekund jazdy zatrzymać samochód?

    c.fill(10);   // napełniamy bak
    c.start();    // ruszamy ...
    System.out.println(c + ""); // co się dzieje z samochodem
    delay(3);     // niech upłyną 3 sek.
    c.stop();     // samochód się zatrzymuje...
    System.out.println(c + ""); // co się dzieje z samochodem
    delay(9);    // niech upłynie jeszcze 9 sek.  od tego momentu
    System.out.println(c + ""); // co się dzieje z samochodem

Wynik nie będzie właściwy, po zatrzymaniu samochodu nadal będzie zużywane paliwo:

Samochód nr rej WA1090 - JEDZIE
Paliwo: 9
Paliwo: 8
Samochód nr rej WA1090 - STOI
Paliwo: 7
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Zatrzymanie samochodu z powodu braku paliwa
Nie jest mozliwe przejscie ze stanu STOI do stanu STOI
Samochód nr rej WA1090 - STOI


Dzieje się tak dlatego, że mimo zatrzymania samochodu (metodą stop()), wątek zużycia paliwa nadal dziala. Tajemniczy komunikat "Nie jest mozliwe przejscie ze stanu STOI do stanu STOI" pojawia się na skutek użycia metody stop() (po wyczerpaniu paliwa, w metodzie run()) wobec pojazdu, który stoi (komunikat jest generowany przez klasę Vehicle).
Niewątpliwie, w momencie zatrzymania samochodu metodą stop() należy zakończyć działanie wątku zużycia paliwa. Musimy zatem mieć jakiś sposób na kończenie działania wątków (o czym w następnym piunkcie).

Warto jeszcze podkreślić, że kod, który ma wykonywać się jako odrębny wątek możemy dostarczyć w klasie wewnętrznej (por. wykład 3).

Przy tworzeniu wątków ad hoc (zwykle tylko raz i na jakąś konkretną potrzebę) bardzo często posługujemy się anonimowymi klasami wewnętrznymi i to zwykle lokalnymi.

Na przykład - do zliczania i pokazywania upływu czasu w trakcie jakiejś interakcji użytkownika z programem, jak w poniższym programie:
import javax.swing.*;

class AnonymousRunnable {
	
  public AnonymousRunnable() {

   Runnable runner = new Runnable() {
     public void run() {
       int time = 0;
       while (true) {
         try {
           Thread.sleep(1000);
         } catch(InterruptedException exc) { return; }
         System.out.println(time++/60 +  " min. " + time%60 + " sek.");
      }
     }
    };

    new Thread(runner).start();
    String s, out = "";
    while ((s = JOptionPane.showInputDialog("Wprowadź jakiś tekst:")) != null)
      out += " " + s;
    System.out.println(out);
    System.exit(0);
    }

  public static void main(String args[]) {
    new AnonymousRunnable();
  }
}

Możliwe są różnorakie schematy użycia (lokalnych) anonimowych klas wewnętrznych.
Niektóre standardowe metody Javy mają jako argument referencję do obiekty typu Runnable i argument ten możemy podawać  jako referencję do obiektu anonimowej klasy wewnętrznej, zdefiniowanej "w miejscu" podania argumentu.


4.3. Kończenie pracy wątku

Wątek kończy pracę w sposób naturalny wtedy, gdy zakończy się jego metoda run().


Jeśli chcemy programowo zakończyć pracę wątku, to  należy zapewnić w metodzie run() sprawdzenie warunków zakończenia (ustalanych programowo) i jeśli są spełnione - spowodować wyjście z run() albo przez "dobiegnięcie do końca", albo przez return.
Warunki zakończenia mogą być formułowane w postaci wartości jakiejś zmiennej, które są ustalane przez inne fragmenty kodu programu (wykonywane w innym wątku).


W klasie Thread znajduje się metoda  stop(), która kiedyś służyła do kończenia działania watków. Stwierdzono jednak, że jej użycie może powodować błędy w programach i w tej chwili nie należy jej już używać.

W naszym samochodowym przykładzie klasa Vehicle (którą dziedziczy Car) dostarcza metody getState(), która zwraca aktualny stan pojazdu (m.in. czy jedzie). Możemy spróbować jej użyć do sprawdzenia warunku kontynuacji działania wątku zużycia paliwa: jeżeli samochód jedzie, to paliwo jest zużywane, w przeciwnym razie wątek zużycia paliwa powinien zakończyć działanie.

    public void run()  {
      while(getState() == MOVING) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        fuel--;
        System.out.println("Paliwo: " + fuel);  // śledzimy ile jest paliwa
        if (fuel <= 0) stop();    // jeżeli brak paliwa...zatrzymujemy samochód
      }
    }
Wynik jest w miarę satysfakcjonujący:
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód nr rej WA1090 - STOI
Paliwo: 6
Samochód nr rej WA1090 - STOI
Oto wydruk testu, w którym - tak jak w końcu poprzedniego podpunktu - po trzech sekundach zatrzymujemy samochód. Okazuje się, że samochód zużył 4 litry paliwa, choć symulowany czas jazdy wynosił ok. 3 sekund.  W innym przebiegu programu mogłoby się okazać, że zużyto trzy (a nie cztery) litry paliwa.  O tym dlaczego tak jest - dowiemy się dalej.

Zwróćmy  teraz uwagę na inny problem, który występuje w naszym programie samochodowym.. Po wprowadzeniu kodu dla wątku zużycia paliwa nasza klasa Car nie jest odporna na błąd dwukrotnego uruchomienia samochodu (wydania samochodowi polecenia start dwa razy).
Owszem, w klasie Vehicle zagwarantowaliśmy brak możliwości przejścia ze stanu MOVING do MOVING (czyli bronimy się przed błędem, i wypisywany jest komunikat "Nie jest możliwe przejście ze stanu JEDZIE do stanu JEDZIE"), ale tworzenie wątku zużycia paliwa w metodzie start() klasy Car nie uwzględnia tego. Napisaliśmy tam po prostu:
new Thread(this).start();
i za każdym razem, kiedy wywołujemy metodę start na rzecz tego samego samochodu tworzony jest i uruchamiany nowy wątek zużycia paliwa (o ile paliwo jeszcze jest).

Modyfikacja :
class Car extends Vehicle implements Runnable {
    ...

    int tnr = 1;  // numer wątku

    // Start samochodu
    public void start()  {
      if (fuel > 0)   {
          super.start();
          new Thread(this, "Nr " + tnr++).start();
      }
      else System.out.println("Brak paliwa");
    }

    ...

    public void run()  {

      Thread cThread = Thread.currentThread();

      while(getState() == MOVING ) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        fuel--;
        System.out.println("Paliwo zużywa wątek: " + cThread.getName() +
                           ", pozostało paliwa: " + fuel);
        if (fuel <= 0) stop();
      }
    }

}

class Test {
  ....
  public static void main(String[] args)  {
    Car c = new Car(...);

    c.fill(10);
    c.start();
    c.start();
    c.start();
    delay(10);
    ...
  }
}

Wynik działania programu pokazuje, że zostały uruchomione trzy wątki zużycia paliwa, co powoduje trzykrotnie szybsze jego wyczerpanie:

Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE // to są komunikaty z klasy Vehicle
Nie jest mozliwe przejscie ze stanu JEDZIE do stanu JEDZIE
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 9
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 8
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 7
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 6
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 5
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 4
Paliwo zużywa wątek: Nr 2, pozostało paliwa: 3
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 2
Paliwo zużywa wątek: Nr 3, pozostało paliwa: 1
Paliwo zużywa wątek: Nr 1, pozostało paliwa: 0


Niewątpliwie musimy zabezpieczyć się przed wielokrotnym uruchamianiem wątków zużycia paliwa dla tego samego samochodu.
Postaramy sie zatem bezpośrednio w klasie Car wprowadzić zmienną, która będzie kontrolować działanie wątku zużycia paliwa. Wygodna okaże się tu zmienna, która reprezentuje sam wątek zużycia paliwa. Jej wykorzystanie pozwala na proste tworzenie i uruchamianie wątku (wtedy, gdy wątek jeszcze nie istnieje), a także łatwe kończenie pracy wątku.

Jest to metoda bardzo często stosowana w programach, gdzie potrzebne jest wielokrotne  uruchamianie i kończenie jakiegoś watku, a jednocześnie trzeba zagwarantować, by w każdym momencie działał tylko jeden wątek, wykonujący kod podanej metody run().

Wprowadzimy zatem do klasy Car definicję zmiennej fuelConsumeThread, która będzie referencją do wątku zużycia paliwa. Inicjalnie będzie ona miała wartość null. Każde wywolanie metody start() wobec samochodu będzie sprawdzać, czy zmienna fuelConsumeThread ma wartośc null. Jeśli tak, to wątek nie istnieje i trzeba go utworzyć i uruchomić. Jeśli nie - wątek już działa i nie należy nic robić.
W metodzie run() możemy pobrać referencję do bieżącego wątku (tego, który aktualnie wykonuje tę metodę run). Metoda run powinna zakończyć działanie, kiedy referencja do bieżącego wątku nie będzie równa zapamiętanej w zmiennej fuelConsumeThread referencji do wątku zużycia paliwa.
Jeśli więc zmiennej fuelConsumeThread nadamy (w wątku głównym programu) wartość null, to uzyskamy trzy efekty:
   
public class Car extends Vehicle implements Runnable {

    ...
    private Thread fuelConsumeThread;
    ...

    // Start samochodu
    public void start()  {
      if (fuel > 0)   {
          super.start();
          if (fuelConsumeThread == null) {
            fuelConsumeThread = new Thread(this);
            fuelConsumeThread.start();
          }
      }
      else System.out.println("Brak benzyny");
    }

    // Zatrzymanie samochodu
    public void stop()  {
        fuelConsumeThread = null;
        super.stop();
    }

    // Zużycie paliwa - wykonuje się jako wątek
    public void run()  {
      Thread cThread = Thread.currentThread();
      while(cThread == fuelConsumeThread ) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        fuel--;
        System.out.println("Pozostało paliwa: " + fuel);
        if (fuel <= 0) stop();
      }
    }

   ....
}

Zwróćmy uwagę, że wątek główny (w którym wykonują się metody start oraz stop, wywoływane na rzecz samochodu) odwołuje się (właśnie w tych metodach) do zmiennej fuelConsumeThread. Do tej samej zmiennej odwołuje się wątek zużycia paliwa. Dwa wątki równolegle operują na tym samym elemencie obiektu.
Wydawałaby się, że nie ma w tym nic zlego. Jednak ze względu na efektywność działania, w Javie wątki operują na kopiach zmiennych, uzgadniając ich wartości z oryginałami tylko w niektórych punktach wykonania programu, tzw. punktach synchronizacji. Zatem metoda stop() lub start() z klasy Car może zmienić wartość swojej kopii zmiennej fuelConsumeThread, a metoda run() - wykonująca się w wątku zużycia paliwa będzie sprawdzać wartość swojej kopii tej zmiennej i nie będzie dostatecznie wcześniej "widziała" dokonanej zmiany.
Aby zabezpieczyć się przed tą konkretną sytuacją można zadeklarować współdzieloną zmienną ze specyfikatorem volatile (co oznacza, że wartości oryginału i kopii zmiennej będą uzgadniane przy każdym odwołaniu do niej) lub też zastosować mechanizmy synchronizacji (inaczej zwane wykluczaniem), bo właśnie w punktach synchronizacji na pewno następuje uzgadnainie kopii z oryginałem. Synchronizacja dzialania watków ma jednak dużo szersze znaczenie i pora teraz na jej omówienie.


4.4. Synchronizacja wątków

Rozważmy następujący przykład.
Oto prosta klasa Balance, z jednym polem - liczbą całkowitą i metodą balance(), która najpierw zwiększa wartość tej liczby, a następnie ją zmniejsza, po czym zwraca wynik - wartość tej liczby.

class Balance {

  private int number = 0;

  public int balance() {
    number++;
    number--;
    return number;
  }

}

Wydaje się nie podlegać żadnej watpliwości, że jakiekolwiek wielokrotne wywoływanie metody balance() na rzecz dowolnego obiektu klasy Balance zawsze zwróci wartość 0.

Otóż, w świecie programowania współbieżnego nie jest to wcale takie oczywiste!
Więcej: wynik różny od 0 może pojawiać się nader często!

Przekonajmy się o tym poprzez wielokrotne wywoływanie metody balance() na rzecz tego samego obiektu w kilku różnych wątkach.

Każdy z wątków będziemy tworzyć i uruchamiać poprzez stworzenie obiektu poniższej klasy BalanceThread, dziedziczącej Thread, i wywołanie na jego rzecz metody start(). Przy tworzeniu nazwiemy każdy z wątków (parametr name konstruktora). Wielokrotne wywołania metody balance() zapiszemy w pętli w metodzie run(). Obiekt na rzecz którego jest wywoływana metoda oraz liczbę powtórzeń pętli przekażemy jako dwa pozostałe argumenty konstruktora. 
Tuż przed zakończeniem metody run() pokażemy jaki był wynik ostatniego odwołania do metody balance().

class BalanceThread extends Thread {

  private Balance b;  // referencja do obiektu klasy Balance
  private int count;  // liczba pwotórzeń pętli w metodzie run

  public BalanceThread(String name, Balance b, int count) {
    super(name);
    this.b = b;
    this.count = count;
    start();
  }

  public void run() {
    int wynik = 0;
    // W pętli wielokrotnie wywołujemy metodę balance()
    // na rzecz obiektu b klasy Balance.
    // Jeżeli wynik metody jest różny od zera - przerywamy działanie pętli
    for (int i = 0; i < count; i++) {
      wynik = b.balance();
      if (wynik != 0) break;
    }
    // Pokazujemy wartość zmiennej wynik na wyjściu z metody run()
    System.out.println(Thread.currentThread().getName() +
                       " konczy z wynikiem  " + wynik);
  }
}
W klasie testującej stworzymy obiekt klasy Balance, po czym stworzymy i uruchomimy podaną przez użytkownika liczbę wątków, które za pomocą metody run() z klasy BalanceThread będą równolegle operować na tym obiekcie wielokrotnie  wywołując na jego rzecz  metodę balance() z klasy Balance.

class BalanceTest {

  public static void main(String[] args) {

    int tnum = Integer.parseInt(args[0]);     // liczba wątków
    int count = Integer.parseInt(args[1]);    // liczba powtórzeń pętli w run()

    // Tworzymy obiekt klasy balance
    Balance b = new Balance();

    // Tworzymy i uruchamiamy wątki
    Thread[] thread = new Thread[tnum];  // tablica wątków
    for (int i = 0; i < tnum; i++)
      thread[i] = new BalanceThread("W"+(i+1), b, count);

    // czekaj na zakończenie wszystkich wątków
    try {
      for (int i = 0; i < tnum; i++) thread[i].join();
    } catch (InterruptedException exc) {
      System.exit(1);
    }
    System.out.println("Koniec programu");
  }

}
Uwaga: metoda join z klasy Thread powoduje oczekiwanie na zakończenie wątku, na rzecz któego została wywołana. Oczekiwanie może być przerwane, gdy wątek został przerwany przez inny wątek - wtedy wystąpi wyjątek InterruptedException.

Uruchamiając aplikację z podanymi jako argumenty liczbą wątkow = 2 oraz liczbą powtorzeń pętli w metodzie run() = 100000, nader często zyskamy intuicyjnie oczekiwany wynik (W1 konczy z wynikiem 0, W2 konczy z wynikem 0). Może się jednak zdarzyć wynik inny! Zwiększenie liczby wątków i liczby powtórzeń pętli prawie na pewno pokaże nam, że niektóre wątki zakończą działanie z wynikem różnym od 0.
Na przyklad, przy liczbie wątkow = 5 i liczbie powtórzeń pętli = 1000000, możemy raz uzyskac następujący wynik:

W2 konczy z wynikiem  0
W3 konczy z wynikiem  0
W4 konczy z wynikiem  0
W1 konczy z wynikiem  0
W5 konczy z wynikiem  0

a za chwilę, przy ponowym uruchomieniu z tymi samymi argumentami:

W1 konczy z wynikiem  1
W3 konczy z wynikiem  1
W2 konczy z wynikiem  1
W5 konczy z wynikiem  0
W4 konczy z wynikiem  0

Testowanie programów wielowątkowych jest trudne, bowiem możemy wiele razy otrzymać wyniki, które wydają się świadczyć o poprawności programu, a przy kolejnym uruchomieniu okaże się, że wynik jest nieprawidłowy. Wyniki uruchamiania programów wielowątkowych mogą być także różne na różnych platformach systemowych.


Powstaje oczywiste pytanie: jak to się dzieje, że w powyższym przykładowym programie uzyskujemy wyniki, których - wydaje się na podstawie analizy kodu metody balance() - nie sposób uzyskać?

Otóż, wszystkie wykonujące (tę samą) metodę run() wątki odwołują się do tego samego obiektu klasy Balance (w programie oznaczanego przez b). Mówimy: współdzielą obiekt.
Obiekt ten ma jeden element - odpowiadający zmiennej number zdefiniowanej jako pole klasy Balance.
Wywolywana przez wątki na rzecz tego obiektu  metoda balance() zwiększa a następnie zmniejsza wartość tej zmiennej.
Wyobraźmy sobie, że działają dwa wątki. Jeden z nich uzyskuje czas procesora i rozpoczyna wykonanie metody balance(). Po zwiększeniu o 1 zmiennej number zostaje wywlaszczony (zmienna number ma teraz wartość 1). Czas procesora przydzielany jest drugiemu wątkowi. Drugi wątek rozpoczyna wykonanie metody balance() i zwiększa o 1 wartość zmiennej number (zmienna number ma teraz wartość 2), po czym zostaje wywlaszczony, a czas procesora przydzielony zostaje wątkowi pierwszemu, który kontynuuje wykonanie metody run() od miejsca, w którym odebrano mu czas procesora. Zmniejsza on teraz zmienną number i zwraca wynik, który równy jest 1. Po zakończeniu wątku pierwszego, pracę kontynuuje wątek drugi. Po zmniejszeniu zmiennej number zwraca wynik 0.

Obrazuje to poniższy rysunek.

rys

Komentarze do rysunku:
Jest to sytuacja, która może się zdarzyć, ale nie musi. Wszystko zależy od tego czy i kiedy (w jakim momecie wykonania metody run()) systemowy zarządca wątków wywlaszczy wykonujący się aktualnie wątek. A z tym jest bardzo różnie w zależności od platformy systemowej czy aktualnego obciążenia procesora.

Zawsze jednak musimy się liczyć z tym, że wątki operujące na wspóldzielonych zmiennych mogą być wywłaszczone w trakcie operacji (nawet pojedyńczej) i wobec tego stan wspóldzielonej zmiennej może okazać się niespójny.


Aby uniknąć równoczesnego działania wątków na tym samym obiekcie (co w sposób nieprzewidywalny ukształtować może jego stany) stosuje się rygle.

Każdy egzamplarz klasy Object i jej podklas posiada rygiel.


Ryglowanie obiektów dokonuje się automatycznie i sterowane jest słowem kluczowym synchronized.
Po części ze względu na to slowo kluczowe mówimy o synchronizowaniu fragmentów kodu, wykonywanych przez wątki.

Synchronizowane mogą być metody i bloki.


Metoda synchronizowana oznaczana jest w deklaracji słowem synchronized:

        synchronized void metoda() {
        ...
        }


UWAGA: słowo kluczowe synchronized nie jest częścią sygnatury metody!
Zatem przedefiniowanie metody synchronizowanej może być synchronizowane albo nie

Kiedy dany wątek wywołuje na rzecz jakiegoś obiektu metodę synchronizowaną, automatycznie zamykany jest rygiel. Mówimy też: obiekt jest zajmowany przez wątek.
Inne wątki usiłujące wywołać na rzecz tego obiektu metodę synchronizowaną (niekoniecznie tę samą, ale koniecznie synchronizowaną) lub też wykonać instrukcję synchronized z podanym odniesieniem do zaryglowanego obiektu (o tej instrukcji za chwilę)  są blokowane i czekają na zakończenie wykonania metody przez wątek, który zajął obiekt (zamknął rygiel).
Dowolne zakończenie metody synchronizowanej (również na skutek powstania wyjątku) zwalnia rygiel, dając czekającym wątkom możność dostępu do obiektu.

W naszym przykładowym programie (z klasami Balance i BalanceThread) dla zapewnienia prawidłowego działania (otrzymywania z metody balance() wyników zawsze równych 0) można zatem zdefiniować metodę balance() w klasie Balance jako synchronizowaną.

class Balance {

  private int number = 0;

  synchronized public int balance() {
    number++;
    number--;
    return number;
  }

}
Wtedy każde wywołanie metody balance() przez jakiś wątek na rzecz obiektu, oznaczanego (w klasie testującej) przez b spowoduje zajęcie obiektu (zamknięcie rygla). Inne wątki starające się równolegle działać na tym samym obiekcie za pomoca metody balance() będą czekać na zakończenie jej wykonania przez wątek, który zajął obiekt. Zatem każdy wątek wykona obie operacje ++ i -- bez ingerencji ze strony innych wątków i wynik metody balance() zawsze będzie równy 0.

Synchronizowanie wątków jest czasowo kosztowne. Łatwo się o tym przekonać porównując czasy wykonania omawianego programu testującego z i bez synchronizacji metody balance().
Ta czasochłonnośc wynika z operacji zamykania i otwierania rygli (które to operacje nie tylko polegają na ustawianiu jakichś flag, ale wiążą się z prowadzeniem i zmianami kolejek oczekującyh na zaryglowanym obiekcie watków). Gdy - w naszym przykładzie - metoda balance() jest synchronizowana, to operacje te - w każdym wątku - wykonywane są tyle razy ile wynosi liczba powtórzeń pętli w metodzie run(). Przy dużej liczbie powtórzeń pętli nasz program może działać bardzo długo.
Pewne zaawansowane sposoby strukturyzacji programów wielowątkowych pozwalają w ogóle unikać synchronizacji przy jednoczesnym wykluczaniu możliwości powstawania niespójnych stanów obiektów.
Należą do nich m.in.:


Bloki synchronizowane.


Bloki synchronizowane wprowadzane są instrukcją synchronized z podaną w nawiasie referencją do obiektu, który ma być zaryglowany.
 
    synchronized (lock) {
        // ... kod
    }

    gdzie: lock - referencja do ryglowanego obiektu
             kod - kod bloku synchronizowanego



Wykonanie instrukcji synchronized przez wątek rygluje obiekt, do którego referencja podana jest w nawiasach tej instrukcji.
Inne wątki, które usiłują operowac na tym obiekcie za pomocą metod synchronizowanych lub wykonać instrukcję synchronized z referencją do tego obiektu są blokowane do chwili gdy wykonanie kodu bloku synchronizowanego nie zostanie zakończone przez wątek zajmujący obiekt.

Bloki synchronizowane są ogólniejsze od metod synchronizowanych, bowiem zapis każdej synchronizowanej metody:

synchronized void metoda() {
   // kod
}

jest równoważny zapisowi:

void metoda() {
    synchronized(this) {
        // kod
    }
}

natomiast za pomocą instrukcji synchronized możemy również synchronizować dowolne fragmenty kodu wewnątrz metod.

W naszym przykładzie "bilansowym" synchronizację wątków możemy uzyskać używając instrukcji synchronized w metodzie run() klasy BalanceThread.

class BalanceThread extends Thread {

  private Balance b;  // referencja do obiektu klasy Balance
  private int count;  // liczba powtórzeń pętli w metodzie run

  public BalanceThread(String name, Balance b, int count) {
    super(name);
    this.b = b;
    this.count = count;
    start();
  }

  public void run() {
    int wynik = 0;
    synchronized(b) {
      for (int i = 0; i < count; i++) {
        wynik = b.balance();
        if (wynik != 0) break;
      }
    System.out.println(Thread.currentThread().getName() +
                       " konczy z wynikiem  " + wynik);
    }
  }
}

Łatwo się przekonać, że to rozwiązanie (w tym konkretnym przypadku) jest lepsze, bowiem ryglowanie obiektu b odbywa się tylko tyle razy ile jest wątków, a nie przy każdym powtórzeniu pętli for.



class SynchroConstr {
    Object lock = new Object(); // semafor
     SynchroConstr() {
         synchronized(lock) {
             // ... kod konstruktora
          }
      }
}

Alternatywnie można użyć synchronizacji na obiekcie-klasie (obiekcie klasy Class, który reprezentuje daną klasę) np.

synchronized(SynchroConstr.class) {
  ...
}

Uwaga: literał  nazwa_klasy.class oznacza obiekt-klasę nazwa_klasy.

Na takich obiektach-klasach są zresztą synchronizowane metody statyczne:

class Klasa {
   public static synchronized void jakasMetoda() { .. }
}

jest równoważne:

class Klasa {
   public static void jakasMetoda()  {
      synchronized(Klasa.class) {
       // ... kod metody
      }
    }


Warto podkreślić, że zawsze powinniśmy mieć pełną świadomość jaki obiekt jest ryglowany. Rozważmy przykład.

class Liczba {
 
   static double n;
   
   synchronized static void set(double x) { n = x; }  // 1
   
   synchronized double get() { return x; }      // 2



Tutaj metoda set jest synchronizowana na obiekcie Liczba.class (obiekcie-klasie), a metoda get - na obiekcie this (obiekcie klasy Liczba, na rzecz którego jest wołana). To są dwa różne obiekty, dwa różne rygle. Zatem nie uzyskujemy wykluczania w dostępie do pola n!

W takich przypadkach powinniśmy użyć synchronizacji na obiekcie Liczba.class (wprowadzając do metody get blok synchronizowany), albo realizować dostęp do pól statycznych wyłącznie za pomocą statycznych metod synchronizowanych.

Podobna sytuacja dotyczy metod definiowanych w klasach wewnętrznych np.

class Outer {
    double n;

     class Inner {
         synchronized void metoda() {
             // ... dostęp do pola n klasy otaczającej
         }
      }
}

nie synchronizuje dostępu do pola n (bo mamy tu synchronizację na obiekcie klasy wewnętrznej, a nie otaczającej).
W tym przypadku należy napisać tak:

       class Inner {
            void metoda() {
                synchronized(Outer.this) {
                    // dostęp do pola n klasy otaczającej
             }
         }

Na koniec warto wspomnieć o problemie synchronizacji przy korzystaniu z singletonów.
Generalnie, powinniśmy metodę statyczną getInstance() w klasie singletonu (zwracającą jedyny obiekt klasy) uczynić synchronizowaną, bowiem jeśli dwa wątki będą równolegle wykonywać tę metodę, to może się zdarzyć, że otrzymamy dwa obiekty klasy (co przeczy definicji singletonu). Faktycznie, w poniższym kodzie:
class KlasaSingletonu {
  private static KlasaSingletonu obj;
  private KlasaSingletonu() {
     // ...
  }   

  public static KlasaSingletonu getInstance() {
    if (obj == null) obj = new KlasaSingletonu();
    return obj;
  }
}  
jeden z wątków może zostać wywłaszczony zaraz po sprawdzania warunku (obj == null), przychodzący na jego miejsce drugi wątek może stworzyć obiekt, a przywrócony pierwszy - "pamiętając", że obiektu nie było - też go stworzy,

Powinniśmy więc napisać tak:
  public static synchronized KlasaSingletonu getInstance() {
    if (obj == null) obj = new KlasaSingletonu();
    return obj;
  }
Warto zauważyć jednak, że taka synchronizacja jest potrzebna tylko przy pierwszym wywołaniu metody getInstance(). Wszystkie inne wywołania będą miały charakter odczytu, nie muszą zatem być synchronizowane, a ponieważ synchronizacja jest kosztowna - to chcielibyśmy jej uniknąć.
Niestety - z tych samych powodów co poprzednio - poniższe rozwiązanie, wydawałoby się logicznie synchronizowane tylko na pierwszym odwolaniu i unikające synchronizacji przy następnych - będzie wadliwe:

  public static KlasaSingletonu getInstance() {
    if (obj == null) { // w tym if (...) mamy ten sam problem co poprzednio
       synchronized(KlasaSingletonu.class) {
         obj = new KlasaSingletonu();
       }
    } 
    return obj;
  }
Aby uniknąć synchronizowanych odwołań powinniśmy zatem stworzyć obiekt przy pierwszym odwołaniu do klasy, co można uzyskać np. tak:

class KlasaSingletonu {

  private static final KlasaSingletonu obj = new KlasaSingletonu();

  private KlasaSingletonu() { // prywatny konstruktor
     // ...
  }   

  public static KlasaSingletonu getInstance() {
    return obj;
  }
}

Podsumowanie synchronizacji:

Synchronizacja jest mechanizmem, który zapewnia, że kilka wykonujących się watków:
Synchronizację wątków uzyskuje się za pomocą obiektów, które wykluczają równoczesny  dostęp wątków do zasobów i/lub równoczesne wykonanie przez wątki tego samego kodu. Takie obiekty nazywają się ogólnie muteksami (od ang. mutual-exclusion semaphore). W Javie rolę muteksów pełnią rygle (ang lock).

O ryglowaniu (wprowadzanym za pomocą słowa kluczowego synchronized)  możemy myśleć jako o zapewnieniu wyłącznego dostępu do pól obiektu lub (statycznych) pól klasy, ale równie dobrze "zaryglowany" obiekt może spelniać rolę muteksu, zabezpieczającego fragment kodu przed równoczesnym wykonaniem przez dwa wątki.

Kod, który może być wykonywany w danym momencie tylko przez jeden wątek nazywa się sekcją krytyczną.
W Javie sekcje krytyczne wprowadza się jako bloki lub metody synchronizowane.
Użycie sekcji krytycznych pozwala na prawidłowe współdzielenie zasobów przez wątki.

W polskiej literaturze przedmiotu używa się także terminów:
Pojęcia te można traktowac jako szczególne przypadki synchronizacji, a ponieważ prawidłowe współdzielenie zasobów jest w programowaniu współbieżnym kluczowe, to często utożsamiamy je z synchronizacją.

Nie należy jednak sądzić, że synchronizacja wątków oznacza zagwarantowanie określonej, konkretnej kolejności dostępu wątków do wspóldzielonych zasobów. Ustalanie i kontrolowanie konkretnej kolejności dostępu wątkow (często zależnej od wyników wytwarzanych przez wykonywane przez nie kody) do wspóldzielonych zasobów będziemy nazywać koordynacją wątków.

4.5. Koordynacja wątków


Ryglowanie (użycie synchronized) służy do zapobiegania niepożądanej interakcji wątków.
Nie jest ono jednak wystarczającym środkiem dla zapewnienia współdziałania wątków.

Przykład:
Dwa wątki Author i Writer mogą odwoływać się do tego samego obiekty typu Teksty.
Author podrzuca teksty, zapisywane w polu txt, Writer wypisuje je na konsoli.
Do ustalania tekstów służy metoda setTextToWrite (wywołuje ją Author), teksty do zapisu odczytywane są przez Writera za pomocą metody getTextToWrite i wypisywane na konsoli.
Ponieważ metody te mogą być wywołane równocześnie (z różnych wątków) i operują na polu tego samego obiektu, winny być synchronizowane.
Ale tu ważna jest również kolejność i koordynacja działań obu wątków.
Chodzi o to, by Writer zapisywał tylko raz to co poda Autor, a Autor nie podawał nic nowego, dopóki Writer nie zapisze poprzednio podanego tekstu.

Skoordynowanie interakcji pomiędzy wątkami uzyskuje się za pomocą metod klasy Object:

Koordynacja działań wątków sprowadza się do następujących kroków:
 

Działanie metod wait, notify, notifyAll zawsze są związane z konkretnymi obiektami, a jednocześnie dotyczy wątków (czyli zazwyczaj innych obiektów), które na tych konkretnych obiektach operują
.


Spójrzmy na przykład  (schemat) prawidłowej koordynacji:

class X {

   int n;
   boolean ready = false;
   ....
   synchronized  int  get() {
       try {
         while(!ready)
           wait();
       } catch (InterruptedException exc) { .. }
       ready = false;
       return n;
   }

   synchronized void put(int i) {
        n = i;
        ready = true;
        notify();
   }

}
Uwaga: metoda wait() może sygnalizować wyjątek InterruptedException (w przypadku, gdy nastąpiło zewnętrzne przerwanie oczekiwania na skutek użycia w innym wątku metody interrupt()). Wyjątek ten musimy obsługiwać.
 
Wyobraźmy sobie, że działają tu dwa wątki - ustalający wartość n za pomocą put i pobierający wartość n za pomocą get.
Wątek pobierający musi czekać, aż wątek ustalający ustali wartość n (wait).
Ustalenie wartości powoduje dwie zmiany: warunek "wartość gotowa" staje się true, a oczekiwanie jest przerywane przez notify.
Zwrócmy uwagę, że metody wait i notify są wywoływane na rzecz tego obiektu, na rzecz którego wywołano metody get i put (moglibyśmy napisać - dla większej jasności: this.wait() i this.notify()).



Oczekiwanie kończy się naprawdę dopiero wtedy, gdy spełniony jest jakiś warunek.

UWAGA: warunek zakończenia oczekiwania należy sprawdzać w pętli. Nie ma bowiem gwarancji, że po odblokowaniu wątku czekającego warunek nadal będzie spełniony.

 
Zgodną z przedstawionym schematem realizację omówionego wcześniej przykładu Author-Writer  pokazano na wydruku poniżej. W programie wątek-Autor co jakiś czas (lgenerowany losowo) ustala tekst do napisania (sa to kolejne elmenty tablicy napisów). Wątek-Writer pobiera ustalony tekst i wypisuje na konsoli. Zakończenie pracy Autor sygnalizuje poprzez podanie tekstu = null.

// Klasa dla ustalania i pobierania tekstów
class Teksty {

  String txt = null;
  boolean newTxt = false;

  // Metoda ustalająca tekst - wywołuje Autor
  synchronized void setTextToWrite(String s) {
    while (newTxt == true) {
      try {
        wait();
      } catch(InterruptedException exc) {}
    }
    txt = s;
    newTxt = true;
    notifyAll();
  }

  // Metoda pobrania tekstu - wywołuje Writer
  synchronized String getTextToWrite() {
    while (newTxt == false) {
      try {
        wait();
      } catch(InterruptedException exc) {}
    }
    newTxt = false;
    notifyAll();
    return txt;
  }

}

// Klasa "wypisywacza"
class Writer extends Thread {

  Teksty txtArea;

  Writer(Teksty t) {
    txtArea=t;
  }

  public void run() {
    String txt = txtArea.getTextToWrite();
    while(txt != null) {
      System.out.println("-> " + txt);
      txt = txtArea.getTextToWrite();
      }
  }

}

// Klasa autora
class Author extends Thread {

  Teksty txtArea;

  Author(Teksty t)  {
    txtArea=t;
  }

  public void run() {

    String[] s = { "Pies", "Kot", "Zebra", "Lew", "Owca", "Słoń", null };
    for (int i=0; i<s.length; i++) {
      try {
        sleep((int)(Math.random() * 1000));
      } catch(InterruptedException exc) { }
      txtArea.setTextToWrite(s[i]);
    }
  }

}

// Klasa testująca
public class Koord {

   public static void main(String[] args) {
     Teksty t = new Teksty();
     Thread t1 = new Author(t);
     Thread t2 = new Writer(t);
     t1.start();
     t2.start();
   }

}




Pojęcie monitora

Każdy obiekt oprócz rygla posiada  "kolejkę oczekiwania" (wait set). Ogólnie kolejka ta zawiera odniesienia do wszystkich wątków zablokowanych na obiekcie (metodą wait) i czekających na powiadomienie o możliwości wznowienia działania. Kolejka oczekiwania jest "prowadzona" przez JVM, a jej zmiany mogą być uzyskane tylko metodami wait, notify, notifyAll (z klasy Object) oraz interrupt (z klasy Thread).
Twory, które mają rygle i kolejki oczekiwania nazywane są monitorami.

Generalnie - monitor jest fragmentem kodu programu (niekoniecznie ciągłym), do którego dostęp zabezpieczany jest przez rygiel (muteks). W odróżnienuiu od sekcji krytycznych - monitory są powiązane z obiektami, ich stanami. Dlatego mówimy czasem krótko: "obiekt ma monitor",  "monitor obiektu" lub nawet "obiekt jest monitorem".


4.6. Stany wątków

Wątek może znajdować się w czterech stanach: New Thread, Runnable, NotRunnable, Dead.

Stan New Thread powstaje w momecie stworzenia obiektu-wątku.

Do stanu Runnable wątek przechodzi po wywołaniu metody start(), która z kolei wywołuje run().

Stan Runnable niekoniecznie oznacza, że wątek jest aktualnie wykonywany. Raczej jest to potencjalna gotowość do działania, a to czy system operacyjny akurat przydzieli temu wątkowi czas procesora zależy od wielu czynników (jaki jeszcze inne wątki konkurują o czas procesora, jaki jest schemat zarządzania wątkami, jakie są ich priorytety itp.).

Do stanu NotRunnable wątek przechodzi na skutek:
Wątek kończy działanie na skutek zakończenia metody run() i przechodzi do stanu Dead.
Mimo, iż nadal może istnieć obiekt oznaczający wątek, ponowne użycie wobec niego metody start() jest niedozwolone (w Javie wątki nie są "restartowalne").
Aby ponownie uruchomić wątek, należy stworzyć nowy obiekt i zastosować metodę start().



4.7. Wstrzymywanie i przerywanie wątków

Działanie wątku może zostać wstrzymane i - później - wznowione. Niegdyś służyły temu metody suspend() i resume() z klasy Thread.
Okazało się, że  są one niespójne i mogą powodować zakleszczanie wątków.
Obecnie do wstrzymywania i przywracania działania wątków proponuje się mechanizm wait-notify.

Można użyć następującego prostego schematu.

Przykładowy schemat wstrzymywania i wznawiania wątków
Wstrzymaniem wątku dyryguje zmienna typu boolean o nazwie np. suspended
W metodzie run() zapisujemy:
   while (warunek_działania_wątku) {
     try {
       synchronized(this) {
         while (suspended)  // warunek wstrzymania wątku
           wait();
       }
     } catch(InterruptedException exc) { return; }
     // ... praca wątku.
   }

Inny wątek - gdy chce wstrzymać działanie tego wątku - ustala suspended = true.
Wznowienie osiągane jest przez suspended = false i notify() na rzecz tego wątku.




Użycie metody interrupt() z klasy Thread powoduje ustalenie statusu wątku jako "przerwany" i - w niektórych sytuacjach - wygenerowanie wyjątku InterruptedException.

Nie należy mylić użycia metody interrupt() - opisywanej zresztą zawsze nieco dwuznacznie jako metody "przerywającej wątek" - z zakończeniem działania wątku.

Ogólnie, metoda ta ustala jedynie status wątku jako przerwany.
Kiedy wątek ze statusem "przerwany" jest (lub zostanie) zablokowany na wywołaniu metod: jego status zmieniany jest na "nieprzerwany" i generowany jest wyjątek InterruptedException.
Jak juz widzieliśmy, przy wywołaniu tych metod wyjątek ten musimy obsługiwać.
W obsłudze wyjątku możemy zdecydować czy wątek ma być zakończony czy nie.

Przykładowy program pokazuje w jaki sposób możemy wstrzymywać, wznawiać, kończyć i przerywać pracę wątku. Działania te próbujemy na wątku, który wypisuje ciąg liczb rzeczywistych (od zera, zwiększanych co 1). Zwróćmy uwagę na użycie metody isAlive() (zwraca true, jesli wątek nie jest w stanie Dead) i isInterrupted() (zwraca true jeśli wątek ma status "przerwany"). Są to jedyne sposoby uzyskania informacji o stanie wątku.
Warto też przetestować program i zobaczyć jakie są konsekwencje różnych poleceń wydawanych przez użytkownika z dialogów wejściowych.

import javax.swing.*;

class SomeThread extends Thread {

  volatile boolean stopped = false;
  volatile boolean suspended = false;

  public void run() {

    double d = 0;
    while(!stopped) {
      try {
        synchronized(this) {
          while (suspended) wait();
        }
      } catch (InterruptedException exc) {
          System.out.println("Obsluga przerwania watku w stanie wait");
      }
      if (suspended) System.out.println("Watek wstrzymany na wartosci " + d);
      else System.out.println(++d);
    }
  }

  public void stopThread() {
    stopped = true;
  }

  public void suspendThread() {
    suspended = true;
  }

  public void resumeThread() {
    suspended = false;
    synchronized(this) {
      notify();
    }
  }

}

class ActionsOnThread {
	
  public static void main(String args[]) {
    String msg = "I = interrupt\n" +
                 "E = end\n" +
                 "S = suspend\n" +
                 "R = resume\n" +
                 "N = new start";

    SomeThread t = new SomeThread();
    t.start();
    String cmd;
    while ((cmd = JOptionPane.showInputDialog(msg)) != null) {
      char c = cmd.charAt(0);
      switch (c) {
        case 'I' : t.interrupt(); break;
        case 'E' : t.stopThread(); break;
        case 'S' : t.suspendThread(); break;
        case 'R' : t.resumeThread(); break;
        case 'N' : if (t.isAlive())
                     JOptionPane.showMessageDialog(null, "Thread alive!!!");
                   else {
                     t = new SomeThread();
                     t.start();
                   }
                   break;
        default  : break;
      }
      JOptionPane.showMessageDialog(null,
                  "Command " + cmd + " executed.\n" +
                  "Thread alive  ? " + (t.isAlive() ? "Y\n" : "N\n") +
                  "Thread interrupted ? " + (t.isInterrupted() ? "Y\n" : "N")
                  );
    }
    System.exit(0);
  }
}


Kompozycja metod wait i interrupt, lub sleep i interrupt może służyć do kończenia pracy wątków.

Obrazuje to poniższy przykład, w którym wątek Increaser zwiększa licznik Counter co freq milisekund, uzyskując efekt powtórzeń za pomocą metody wait(..), która wstrzymuje działanie wątku na podany czas.
import javax.swing.*;

class Counter {
  private double val;
  public void incr() { val++; }
  public String toString() { return String.valueOf(val); }
}

class Increaser extends Thread {

  private Counter count;  // zwiększany licznik
  private int freq;       // częstotliwość zwiększania

  Increaser(Counter c, int f) {
    super("Increasing thread");
    freq = f;
    count = c;
    start();
  }

  public void run() {
    while (true) {
      synchronized(count) {
        try {
          count.wait(freq);                // czeka freq ms
        } catch (InterruptedException exc) {
            System.out.println(getName() + " interrupted.");
            return;
        }
       count.incr();               // zwiększenie licznika
       System.out.println(count);  // pokaz wartość licznika
      }
    }
  }
}

class WaiterTest {

  public static void main(String[] args) {
    Counter counter = new Counter();
    Increaser inc = new Increaser(counter, 1000);
    int rc = JOptionPane.showConfirmDialog(null, "Czy zakończyć wątek?");
    // Zakończenie pracy wątku za pomocą netody interrupt
    if (rc == 0) inc.interrupt();
    System.exit(0);
  }
Tutaj niepotrzebne jest notify dla wznowienia wątku, bo dochodzi do niego automatycznie po upływie czasu podanego w  metodzie wait. W każdym momencie, z zewnątrz, można za pomocą metody interrupt ustawić status wątku Increaser jako przerwany. Gdy wątek jest zablokowany (wait jeszcze nie wróciło), to oczekiwanie jest przerywane i generowany jest wyjątek InterruptedException . Gdy natomiast w momencie wykonania  interrupt wątek się wykonywał, to wyjątek zostanie zgłoszony natychmiast po kolejnym wejściu w wait. W obsłudze wyjątku kończymy wykonanie metody run.  Pozwala to kończyć wątki  w sposób niezależny od częstotkiwości wykonania (wynikającej z czasu podanego w wait) czyli praktycznie natychmiast, jesli tylko instrukcje pomiędzy kolejnymi wait są wykonywane bardzo szybko.  

Ten sam efekt możemy uzyskać, używając - np. w metodzie run() klasy Increaser - zamiast wait(freq) - sleep(freq).
Należy jednak pamiętać, że wait(...) zwalnia rygiel obiektu (udostępniając go innym wątkom synchronizowanym na tym obiekcie), a sleep - nie.

Działanie wait i sleep możemy porównać modtfikując metodę main z poprzednidgo przykładu i uruchamiając program w dwóch wersjach: z wait w metodzie run klasyIncreaser oraz zastępując wait w tej metodzie - wywołaniem metody sleep.

  public static void main(String[] args) {
    int freq = 3000;
    Counter counter = new Counter();
    Increaser inc = new Increaser(counter, freq);
    try { Thread.sleep(freq/3); } catch (Exception exc) { }
    long startTime = System.currentTimeMillis();
    synchronized(counter) {
      counter.incr();        // próba zwiększenia licznika z tego wątku
    }
    System.out.println("Licznik " + counter +  // jaki wynik? ile to trwało?
                       "\nCzas spędzony na counter.incr() " +
                       (System.currentTimeMillis() - startTime));
  }
Przy użyciu metody wait() w run() klasy Increaser otrzymamy wydruk: Przy użyciu metody sleep() w run() klasy Increaser otrzymamy wydruk:
Licznik 1.0
Czas spędzony na counter.incr() 0
2.0
3.0
4.0

1.0
Licznik 2.0
Czas spędzony na counter.incr() 1970
3.0
4.0




Oba typy zachowań mogą być potrzebne w różnych sytuacjach. Gdy stosujemy opisany mechanizm wyłącznie do oddawania czasu procesora i zapewnienia przy tym możliwości zewnątrznego kończenia wątku, otwieranie rygla i udostępnianie jakiegoś wspóldzielonego zasobu (obiektu) innym wątkom (czyli stosowanie wait) może prowadzić do niespójności. Zatem, albo nalezy synchronizować wait na obiekcie, który na pewno nie będzie wspóldzielony albo stosować sleep. Jednak  usypianie wątku za pomocą sleep w sekcji krytycznej może doprowadzić do zablokowania lub bardzo niefektywnego działania programu (bowiem w trakcie uśpienia wątku - inne wątki synchronizowane na obiekcie nie będą mogły się wykonywać). Trzeba zatem dobierać w sleep odpowiednio małe takty czasowe.

Zobaczmy przykłąd zablokowania programu. Wprowadzimy tu, oprócz omawianej klasy Increaser (z użyciem metody wait w run) - nową klasę Sleepy, która reprezentuje wątek synchronizowany na tym samym obiekcie co "obliczeniowy" kod metody run() w klasie Increaser. Wątek Sleepy w którymś momencie uśnie na długi czas.

 class Sleepy extends Thread {

  private Counter count;
  private final int N = 5;

  public Sleepy(Counter c) {
    count = c;
    start();
  }

  public void run() {
    for (int i=1; i <= N; i++) {
      // w ostatniej iteracji usypiamy na długo
      int time = (i == N ?  100000 : 1000);
      synchronized(count) {
        System.out.println("Sleep at " + count);
        try {
          sleep(time);
        } catch (InterruptedException exc) {
        }
      }
    }
  }
}
Oba wątki Increaser i Sleepy są uruchamiane równolegle w metodzie main.
    int freq = 3000;
    Counter counter = new Counter();
    Increaser inc = new Increaser(counter, freq);
    new Sleepy(counter);
Wynik jest taki, że po upływie kilku sekund i wyprowadzeniu komunikatów:

Sleep at 0.0
Sleep at 0.0
Sleep at 0.0
1.0
Sleep at 1.0
Sleep at 1.0


program sprawia wrażenie zamrożonego, bowiem wątek  Increaser jest zablokowany na zajętym przez "śpiocha" obiekcie counter.

Zauważmy, że za pomocą kombinacji wait-interrupt (bezpieczniejszej od sleep-interrupt) można próbować tworzyć bardziej uniwersalne mechanizmy kończenia wątków.
Zobaczmy to na przykładzie klasy IterruptibleRepetitiveThread, która służy do powtarzalnego wykonywania jakichś działań w wątku, przy jednoczesnym zapewnienu możliwości kończenia pracy wątku za pomocą użycia na jego rzecz metody interrupt. Działania do wykonania będą opisywane za pomocą implementacji metody execute() interfejsu Command w konkretnych klasach. Przy tworzeniu obiektów-wątków dostarczymy w konstruktorze obiekt klasy implementującej interfejs Command a także częstotliwość z jaką ma być powtarzana metoda execute() tej klasy. Po każdyym wykonaniu execute (a także w oczekiwaniu na kolejne wykonanie) będzie można zakończyć działanie wątku.

// Implementacja tego interfejsu w jakiejś klasie
// dostarczy definicji metody do wykonania w wątku
interface Command {
  public void execute();
}

// Klasa przerywalnych wątków
// wykonujących powtarzalne czynności
class InterruptibleRepetitiveThread extends Thread {

  // obiekt dostarczający definicji czynności
  private Command command;

  // Muteks do synchronizacji wait
  // Potrzebny jedynie dla zapewnienia reguł składniowych
  // Obiekt ten na pewno nie będzie wspóldzielony!
  private final Object waitMutex = new Object();

  // Częstotliwość powtarzania czynności
  int freq;

  public InterruptibleRepetitiveThread(String name, int f, Command com) {
    super(name);
    command = com;
    freq = f;
  }

  public void run() {
    // Tu pętla nieskończona, można dodać warianty
    // np. powtórzenie ileś razy
    while (true) {
      // Wykonanie czynności.
      // Założenie: synchronizacja dostępu do jakichś wspóldzielonych
      // obiektów - na poziomie definicji metody execute()!!!
      command.execute();

      // Umożliwienie zakończenia pracy wątku
      synchronized(waitMutex) {
        try {
          waitMutex.wait(freq);
        } catch (InterruptedException exc) {
            System.out.println(getName() +  " - interrupted");
            return;
        }
       }
    }
  }

}
Klasę testująca działanie na przykładzie wątków współdzielących obiekct liczniku pokazano na wydruku. Zwróćmy uwagę na konieczność synchromizacji na poziomie definicji metod execute.

class Counter {
  private double val;
  public void increase() { val++; }
  public String toString() { return String.valueOf(val); }
}

class InterruptibleTest {

  Counter counter = new Counter(); // wspóldzielony licznik

  InterruptibleTest() {

    // Tworzymy i uruchamiamy piewrszy wątek
    Thread licz1 = new InterruptibleRepetitiveThread("Licznik 1", 3000,
        new Command() {
          public void execute() {   // konieczna synchronizacja
            synchronized(counter) {
              counter.increase();
              System.out.println("Licznik 1 - " + counter);
            }
          }
        });

    licz1.start();

    // Drugi wątek
 
    Thread licz2 = new InterruptibleRepetitiveThread("Licznik 2", 3000,
        new Command() {
          public void execute() {
            synchronized(counter) {   // konieczna synchronizacja
              counter.increase();
              counter.increase();
              counter.increase();
              System.out.println("Licznik 2 - " + counter);
            }
          }
        });

    licz2.start();

    // Dialog umożliwiający zakończenie wątków
 
    String[] opcje = { "Licznik 1", "Licznik 2", "Oba liczniki" };

    // Dopóki działa któryś z wątków
    while (licz1.isAlive() || licz2.isAlive()) {
      int rc = JOptionPane.showOptionDialog(null, "Który wątek zakończyć?",
                                   null, 0, 0, null, opcje, opcje[2]);
      switch (rc) {
        case 0 : licz1.interrupt(); break;
        case 1 : licz2.interrupt(); break;
        case 2 : licz1.interrupt();
                 licz2.interrupt();
                 break;
        default : break;
      }
      Thread.currentThread().yield();  // samowywłaszczenie
    }
    System.exit(0);
  }

  public static void main(String[] args) {
     new InterruptibleTest();
  }

}
Działanie programu ilustruje rysunek.
r


Warte podkreślenia w pokazanym przykładzie testowym są dwie kwestie.
Po piewrsze,  trzeba zapewnić synchronizację w dostępie do obiektu counter, ale  zastosowanie synchronizacji metody execute (synchronized execute) nie spełnia tego zadania, bo metoda ta jest definiowana w klasie wewnętrznej i byłaby synchronizowana na jej obiekcie, a nie na obiekcie klasy Counter.
Po drugie, przed ponownym wyświetleniem dialogu należy dobrowolnie oddać czas procesora, aby metody isAlive właściwie odczytały stan wątków (to, że jakichś z nich został zakończony). Nie było sensu usypiać wątku głównego (pokazującego dialogi), wystarczyło posłużyć się "lekką" odmianą samowywłaszczenia - wywołaniem metody yield(). Nie przesuwa ona wątku do stanu NotRunnable (jak sleep czy wait), ale powoduje oddanie czasu procesora (jednak tylko jeśli czekające wątki mają równy lub wyższy priorytet od tego który zrzeka się - na chwilę - dostępu do procesora).





4.8. Priorytety wątków, granulacja czasu i samowywłaszczanie

Przyjrzyjmy się jeszcze raz działaniu wątków, dodając do wcześniej pokazanej klasy Car metodę getFuel(), zwracającą stan paliwa i testując klasę Car w poniższej klasie.
public class TestCarE {
   static int runTime;

   static void delay(Car c, int sek) {
     if (c.getState() == Car.MOVING) runTime += sek;
     System.out.println(c  + " przez " + sek + " sek....");
     while(sek-- > 0) {
       try {
         Thread.sleep(1000);
       } catch (Exception exc)  { }
     }
   }

 public static void main(String[] args)  {
    Car c = new Car("WA1090", new Person("Janek", "0909090"),
                     100, 100, 100, 100, 50);

    c.fill(10);   // napełniamy bak
    c.start();    // ruszamy ...
    delay(c, 3);  // niech upłynie 3 sek. jazdy od tego momentu
    c.stop();     // zatrzymujemy samochód
    delay(c, 1);  // czekamy sekundę...
    c.start();    // uruchamiamy samochód itd...
    delay(c, 2);
    c.stop();
    delay(c, 2);
    c.start();
    delay(c, 4);
    c.stop();
    System.out.println("Zużycie paliwa: " + (10 - c.getFuel()) +
                       " w czasie jazdy " + runTime + " sek.");
 }
}
Metoda delay(...) nie tylko symuluje upływ czasu, ale również informuje jaki jest stan pojazdu (stoi, jedzie) w tym czasie oraz zlicza czas "czystej" jazdy (zmienna runTime). Na końcu programu pokazujemy ile zużyto paliwa w ciągu ilu sekund jazdy.

Okaże się, że uruchamiając kilka razy ten program otrzymamy różne wyniki.
Możemy np. otrzymac i taki:

Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 6
Paliwo: 5
Samochód STOI przez 2 sek....
Paliwo: 4
Samochód JEDZIE przez 4 sek....
Paliwo: 3
Paliwo: 2
Paliwo: 1
Paliwo: 0
Nie jest mozliwe przejscie ze stanu STOI do stanu STOI
Zużycie paliwa: 10 w czasie jazdy 9 sek.


Aby wytłumaczyć ten wynik - przypomnijmy fragment klasy Car:
    public void stop()  {
      fuelConsume = null;
      super.stop();
    }

    public void run()  {
      Thread cThread = Thread.currentThread();
      while(cThread == fuelConsume) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        fuel--;
        System.out.println("Paliwo: " + fuel);  // śledzimy ile jest paliwa
        if (fuel <= 0) stop();    // jeżeli brak paliwa...zatrzymujemy samochód
      }
    }
Jak widać, wątek zużycia paliwa usypia na sekundę. W tym czasie może się zdażyć, że główny wątek - za pomocą metody stop(), użytej wobec obiektu klasy Car zatrzymał samochód. Po obudzeniu wątek zużycia paliwa nic o tym nie wie i wykonuje operację fuel-- (zmniejszając paliwo, mimo, że samochód stoi).

Wydaje się, że ta sytuacja  wynika z braku synchronizacji. Istotnie, mamy tu do czynienia ze zmienną  wspóldzieloną przez dwa wątki (fuelConsume).

Synchronizowanie w metodzie stop() jest oczywiste:
    synchronized(this) {
        fuelConsume = null;
    }

w metodzie run() wymaga natomiast lekkiej przebudowy: w czasie kiedy wątek zużycia paliwa odlicza sekundę i zmniejsza paliwo, żaden inny wątek nie powinien mieć dostępu do zmiennej fuelConsume.

    public void run()  {
      Thread cThread = Thread.currentThread();
      while(true) {
        synchronized(this) {
          if (cThread != fuelConsume) break;
          try {
            Thread.sleep(1000);
          } catch(InterruptedException exc) { return; }
          fuel--;
          System.out.println("Paliwo: " + fuel);  // śledzimy ile jest paliwa
        }
        if (fuel <= 0) stop();    // jeżeli brak paliwa...zatrzymujemy samochód
      }
    }

Efekt ten uzyskamy dzięki właściwości metody sleep(..).

Użycie metody sleep(...) przesuwa wątek do stanu NOT RUNNABLE, ale nie zwalnia zamkniętych rygli


Skądinąd, właściwość ta jest jednak niebezpieczna, może bowiem spowodować niepotrzebne, zbyt długie, oczekiwanie innego wątku na ryglu zamkniętym przez śpiący wątek.

Właśnie dlatego prezentowana wyżej synchronizacja nie rozwiąże naszego problemu: nadal  możemy otrzymywać wyniki zużycia 10 litrów nbenzyny w 9 sekund jazdy.
W kontekście np.
    delay(c, 2);
    c.stop();
scenariusz może być taki:
po obudzeniu głównego wątku (uśpionego w metodzie delay) główny watek nie otrzymuje od razu czasu procesora, wykonuje się natomiast kolejna iteracja pętli wątku zużycia paliwa. Wprowadzana jet znowu sekcja krytyczna i następuje uśpienie wątku zużycia paliwa. Kontrolę zyskuje wątek główny, ale metoda stop() czeka na zaryglowanym obiekcie. Wątek zużycia paliwa budzi się po sekundzie, zmniejsza paliwo i po wyjściu z sekcji krytycznej oddaje sterowanie wątkowi głównemu Dopiero teraz wykonywane jest stop(), które przerwie działanie wątku zużycia paliwa. W takiej sytuacji łatwo zgubić "trochę" paliwa.

Skoro synchronizacja nie rozwiązuje problemu nadmiernego zużycia paliwa, to spróbujmy poradzić sobie inaczej,

Przede wszystkim zadeklarujemy zmienną fuelConsume jako volatile (aby zapewnić uzgadnianie przez watki ich kopii z oryginałem przy każdym odwołaniu do zmiennej).
volatile private Thread fuelConsume;
Powodem naszych kłopotów było pierwotnie (kiedy jeszcze nie zapewnialiśmy wykluczania) to, iż w czasie uśpeinia wątku zużycia paliwa wątek główny mógł wykonać metodę stop() i nadać zmiennej fuelConsume wartośc null, a po obudzeniu wątek zużycia paliwa nie sprawdzał tego i zmniejszal paliwo. Zatem sprawdźmy czy po obudzeniu wątku zużcyia paliwa przyapadkiem samochód nie jest już zatrzymany.

public void run()  {
      Thread cThread = Thread.currentThread();
      while(cThread == fuelConsume) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        if (cThread == fuelConsume) { // zmniejszamy paliwo tylko jeśli nie wykonano stop
          fuel--;
          System.out.println("Paliwo: " + fuel);  // śledzimy ile jest paliwa
        }
        if (fuel <= 0) stop();    // jeżeli brak paliwa...zatrzymujemy samochód
      }
    }
Niestety i tym razem wynik nie będzie dobry. Może się okazać, że w ciągu 9 sekund jazdy zużyto tylko 6 lub 7 litrów paliwa. Np.

Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 7
Samochód STOI przez 2 sek....
Samochód JEDZIE przez 4 sek....
Paliwo: 6
Paliwo: 5
Paliwo: 4
Paliwo: 3
Zużycie paliwa: 7 w czasie jazdy 9 sek.


Zauważmy, że w naszym programie samochodowym dwa wątki usypiają co pewien czas na sekundę. Po obudzeniu nie ma gwarancji, że wątek dostanie natychmiast czas procesora (do którego konkuruje wiele innych wątków z różnych przecież procesów). Może się więc zdarzyć, że wątek zużycia paliwa nie dostanie czasu procesora zaraz po obudzeniu,  a wątkowi głównemu (w którym wykonywana jest metoda delay()) "równolegle" uda się zwiększyć licznik upływającego czasu. Efektem jest zbyt małe z kolei zużycie paliwa w stosunku do czasu jazdy.

Powinniśmy więc w bardziej subtelny sposób próbować kontrolować czas przydzielany poszczególnym wątkom.

Mamy po temu dwie możliwości:

Priorytet wątku jest liczbą całkowitą. Wątki o wyższym priorytecie powinny od systemowego zarządcy wątków uzyskiwać częstszy dostęp do procesora


Priorytety wątków odgrywają rolę przy ich szeregowaniu.

Szeregowaniem wątków nazywa się ustalenie kolejności wykonania (dostępu do procesora) wielu wątków  na maszynie jednoprocesorowej


JVM stosuje deterministyczny algorytm szeregowania wątków zwany szeregowaniem stało-priorytetowym i bazującym na priorytetach wątków.

Gdy uruchamiamy nasz program, tworzony jest i uruchamiany jego wątek główny i w nim wykonywana jest metoda main(...). Wątek ten ma priorytet oznaczony stałą Thread.NORMAL_PRIORITY (obecnie 5).
W wątku głównym możemy tworzyć inne wątki, a w tych innych - jeszcze inne. Gdy tworzony jest nowy watek - uzyskuje on ten sam priorytet, co wątek w którym został stworzony.
Po utworzeniu wątku możemy zmienić jego priorytet za pomocą metody setPriority(int).
Dostępny zakres priorytetów obejmuje liczby z przedziału Thread.MIN_PRIORITY... Thread.MAX_PRIORITY (obecnie 1-10).

Po utworzeniu wątku, wątek znajduje się w stanie NEW THREAD i nie jest jeszcze gotowy do wykonania. Dopiero użycie metody start() na rzecz wątku przeprowadza go w stan gotowości do wykonania - zwany RUNNABLE. Zob. stany wątków.

Bycie w stanie RUNNABLE nie znaczy, że wątek się wykonuje. Może on czekać w kolejce innych wątków na przydzielenie czasu procesora przez zarządcę wątków


Gdy w danym momencie czasu wiele wątków gotowych jest do wykonania (czeka w kolejce na czas procesora) zarządca wątków wybiera wątek z najwyższym priorytetem i jemu przydziela czas.
Gdy czekają wątki o tym samym priorytecie - wybierany jest jeden z nich.
Wybrany wątek wykonuje się dopóki:
Następnie zarządca wątków przydziela czas kolejnemu wątkowi z kolejki oczekujących na czas procesora. I tak "w koło Macieju".

Możemy zatem spróbować zapewnić właściwe (cosekundowe) zużycie paliwa ustalając dla wątku zużycia paliwa priorytet wyższy od priorytetu wątku z którego zostaje on uruchomiony.


Do ustalania priorytetów watków sluży  metoda klasy Thread

        setPriority(int prior)

     gdzie prior - priorytet wątku

Priorytet danego wątku możemy odczytać natomiast za pomocą metody getPriority().   



Ustalmy maksymalny priorytet. Istotne fragmenty kodu pokazano poniżej:
class Car extends Vehicle implements Runnable {

    // ...
    volatile private Thread fuelConsume;
    // ..

    // Start samochodu
    public void start()  {
      if (fuel > 0)   {
          super.start();
          if (fuelConsume == null) {
            fuelConsume = new Thread(this);
            fuelConsume.setPriority(Thread.MAX_PRIORITY);
            fuelConsume.start();
          }
      }
      else System.out.println("Brak paliwa");
    }

    // Zużycie paliwa
    public void run()  {
      Thread cThread = Thread.currentThread();
      while(cThread == fuelConsume) {
        try {
          Thread.sleep(1000);
        } catch(InterruptedException exc) { return; }
        if (cThread == fuelConsume) {
          fuel--;
          System.out.println("Paliwo: " + fuel);
          if (fuel <= 0) stop();
        }
      }
    }

    // Zatrzymanie samochodu
    public void stop()  {
        fuelConsume = null;
        super.stop();
    }
    // ...
}
a wynik - przynajmniej na platformach Windows 98 i OS/2 -  będzie nareszcie precyzyjny:

Samochód JEDZIE przez 3 sek....
Paliwo: 9
Paliwo: 8
Paliwo: 7
Samochód STOI przez 1 sek....
Samochód JEDZIE przez 2 sek....
Paliwo: 6
Paliwo: 5
Samochód STOI przez 2 sek....
Samochód JEDZIE przez 4 sek....
Paliwo: 4
Paliwo: 3
Paliwo: 2
Paliwo: 1
Zużycie paliwa: 9 w czasie jazdy 9 sek.


Nie możemy jednak twierdzić, że jest to uniwersalne rozwiązanie.
Wszystko zależy od tego w jaki sposób systemowy zarządca wątków przydziela im czas procesora oraz czy i na ile uwzględnia priorytety wątków ustalane przez Javę.
Np. w różnych systemach operacyjnych są różne liczby priorytetów. Priorytety Javy są odwzorywywane na priorytety uwzględniane przez systemowego zarządcę watków, co może prowadzić do innego od oczekiwanego (na poziomie programu napisanego w Javie) szeregowania wątków.

Wobec tego:

W progaramch wieloplatformowych należy unikać stosowania priorytetów wątków. Ustalanie priorytetów może być natomiast pomocne przy szczegółowym dostrajaniu działania programu na danej platformie systemowej


Czy istnieje inny niż zastosowanie priorytetów sposób rozwiązania naszego problemu?
Wydaje się, że podobne efekty  - ale już niezależne od sposobu systemowego odwzorowania priorytetów - możemy osiągnąć poprzez  bardziej precyzyjne kwantowanie czasu i sprawienie, by wątek, który uznajemy za mniej "krytyczny" częściej dobrowolnie oddawał czas procesora.

W naszym programie samochodowo-paliwowym będzie to wątek główny (bo kłopoty wynikały z tego, że wątek zużycia paliwa "nie nadążał" ze zmniejszaniem jego ilości).
Wątek główny dobrowolnie oddaje czas na skutek wywołania metody sleep z metody delay. Usypia na sekundę, zatem co sekundę następuję jego obudzenie i wobec tego co sekundę następuje też oddanie przez niego czasu procesora.
A przecież w metodzie delay możemy zliczać sekundy za pomocą bardziej precyzyjnego kwantowania czasu, na przykład dzieląc sekundę na 10 części. Wtedy dziesięć razy częściej wątek zużycia paliwa będzie miał szansę dostępu do procesora, bowiem w ciągu sekundy nie raz, a dziesięć razy wątek główny będzie przesuwany do stanu NOT RUNNABLE.

Zatem: usuwamy ustalenie wysokiego priorytetu wątku zużycia paliwa z metody start() klasy Car, a zamiast tego modyfikujemy metodę delay(), która teraz będzie powodowac dziesięciokrotne w ciągu sekundy dobrowolne wywłaszczenie watku głównego.

   static int runTime;

   static void delay(Car c, int sek) {
     if (c.getState() == Car.MOVING) runTime += sek;
     System.out.println(c  + " przez " + sek + " sek....");
     int n = sek*10;
     while(n-- > 0) {
       try {
         Thread.sleep(100);
       } catch (Exception exc)  { }
     }
   }
Okaże się, że teraz możemy  także uzyskać dokładne wyniki, dotyczące zużycia paliwa.
Ale znowu, taka granulacja czasu, dostrajanie kwantów może zależeć od systemu operacyjnego.

Można wszakże mieć nadzieję, że omówione tu sposoby postępowania okażą się przydatne przy rozwiązywaniu praktycznych problemów.




4. 9. Demony i grupy

Demony - to wątki, których przeznaczeniem jest wykonywanie swoistych prac systemowych lub pomocniczych w tle. Podstawową różnicą pomiędzy zwykłymi wątkami i wątkami-demonami jest to, iż program nie może skończyć działania w sposób naturalny o ile działa jeszcze jakiś zwykły wątek (tzw. watek uzytkownika, user thread), natomiast wątki-demony automatycznie kończą działanie wraz z zakończeniem programu i nie wstrzymują zamknięcia aplikacji,


Do ustalenia rodzaju wątku służy metoda setDaemon(boolean), która musi być wywołana przed uruchomieniem wątku,

Wątki są łączone w grupy. Każdy wątek należy do jakiejś grupy. Możemy też tworzyć nowe grupy wątków i dołączac do tych grup nowotworzone watki. Służy do tego klasa ThreadGroup (oraz argument konstruktora klasy Thread). Możemy uzyskiwać wątki należące do grupy i wykonywać różne opercaje "od razu" na wszystkich wątkach grupy.

Zobaczmy na przykładzie. Poniższy program pokazuje wszystkie wątki biężacej grupy, po czym kończy wszystkie wątki, które pochodzą od omówionej wcześniej klasy InterruptibleRepetitiveThread (zob. punkt. 14.9) i znowu pokazuje wszystkie wątki. Przy okazji prześledzimy dokładnie wątki tworzone w programie i zobaczymy dlaczego użycie dialogów JOptionPane wymaga  kończenia aplikacji za pomocą System.exit().



import javax.swing.*;

class ThreadReporter {

  // Jako argument podajemy tryb, określający czy mamy
  // korzystać z elementó GUI
  // od tego trybu zależy ile wątków będzie działać
  // w naszym programie  
  public ThreadReporter(String mode) {

    if (mode.equals("Frame")) {
      JFrame f = new JFrame();
    }

    else if (mode.equals("Dialog"))
      JOptionPane.showMessageDialog(null, "Zobaczmy!");

    showThreads();
    try { Thread.sleep(3000); } catch(Exception exc) {}
    tryToInterruptAllInterruptible();
    try { Thread.sleep(3000); } catch(Exception exc) {exc.printStackTrace();}
    showThreads();

  }

  // Metoda zwraca tablicę aktywnych wątków w bieżącej grupie
  public Thread[] getActiveThreads() {
    ThreadGroup tgr = Thread.currentThread().getThreadGroup();

    // Uwaga: activeCount() daje estymowaną liczbę wątków w grupie
    Thread[] t = new Thread[tgr.activeCount()];

    // Metoda enumerate - wypelnia tablicę wątków
    int n = tgr.enumerate(t, true);

    // Działające wątki
    Thread[] realThreads = new Thread[n];
    for (int i=0; i<n; i++) realThreads[i] = t[i];
    return realThreads;
  }

  // Pokazuje informacje o dizłalających wątkach
  public void showThreads() {
    Thread[] t = getActiveThreads();
    System.out.println("Liczba aktywnych wątków " + t.length);
    for (int i=0; i < t.length; i++) {
      System.out.println(t[i].getName() + ", priority = " +
                         t[i].getPriority() +
                         (t[i].isDaemon() ? " daemon " : " user ") + "thread"
                        );
    }
  }

  // Kończy wszystkie wątki, które należą do klasy
  // InterruptibleRepetitiveThread
  public void tryToInterruptAllInterruptible() {
    Thread[] t = getActiveThreads();
    for (int i=0; i < t.length; i++) {
      if (t[i] instanceof InterruptibleRepetitiveThread)
         t[i].interrupt();
    }
  }


  public static void main(String[] args) {

    String s = (args.length == 0 ? "none" : args[0]);

    new InterruptibleRepetitiveThread("Gwiazdki", 1000,
        new Command() {
          public void execute() {
            System.out.println("***");
          }
        }).start();

    new InterruptibleRepetitiveThread("Plusy", 1000,
        new Command() {
          public void execute() {
            System.out.println("+++");
          }
        }).start();

    new ThreadReporter(s);

  }
}

W zależności do tego z jakim argumentem uruchomimy program - zostanie stworzona  różna liczba wątków. Jeśli nie podamy żadnego argumentu, będziemy mieli - oprócz głównego (o nazwie main) - tylko dwa własne wątki "Gwiazdki" i "Plusy", po zakończeniu których aplikacja zakończy wątek główny i zakończy działanie:

Liczba aktywnych wątków 3
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
+++
***
***
+++
Plusy - interrupted
Gwiazdki - interrupted
Liczba aktywnych wątków 1
main, priority = 5 user thread


Jeśli podamy argument Frame, to zostanie stworzone okno JFrame, co spowoduje (niejawnie) utworzenie dwóch dodatkowych wątków, związanych z GUI.

Liczba aktywnych wątkąw 5
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
***
+++
***
+++
***
+++
Gwiazdki - interrupted
Plusy - interrupted
Liczba aktywnych wątków 2
main, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread


Aplikacja również zakończy działanie, gdyż wątek AWT-Shutdown zamyka ją i kończy wykonanie, a pozostający AWT-Windows jest demonem i automatycznie kończy się wraz z zakończeniem wątku głownego.

Wreszcie - gdy za pomocą argumentu - Dialog, spowodujemy otwarcie dialogu JOptionPane, to pojawią sie jeszcze dwa dodatkowe wątki: AWT-EventQueue (do obsługi 
kolejki zdarzeń) oraz ImageFetcher (do pobierania obrazów, co  jest potrzebne bo dialog zawiera ikonę). Teraz, po zamknięciu dialogu wątek ImageFetcher automatycznie zakończy działanie, a nasze wątki zostaną zakończone przez naszą metodę tryToInterrupt...
Jednak ponieważ wśród pozostałych wątków znajduje się wątek AWT-EventQueue, który nie jest demonem, to po zakończeniu wątku głownego  nasza aplikacja nie skończy dzialania.
Liczba aktywnych w¦tkˇw 7
main, priority = 5 user thread
Gwiazdki, priority = 5 user thread
Plusy, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
Image Fetcher 0, priority = 8 daemon thread
AWT-EventQueue-0, priority = 6 user thread
***
+++
***
+++
+++
***
Gwiazdki - interrupted
Plusy - interrupted
Liczba aktywnych w¦tkˇw 4
main, priority = 5 user thread
AWT-Shutdown, priority = 5 user thread
AWT-Windows, priority = 6 daemon thread
AWT-EventQueue-0, priority = 6 user thread


Dlatego wlaśnie w takich przypadkach musimy używać System.exit(..).




4. 10. Timery


Klasa Timer służy do uruchamiania zadań, określanych przez obiekty klasy TimerTask w określonym czasie i/lub z określoną częstotliwością.
Zdefiniowanie zadania do wykonania polega na odziedziczeniu klasy TimerTask i dostarczeniu kodu do wykonania w metodzie run(). Następnie za pomoca metody schedule klasy Timer możemy zlecić, by kod ten został wykonany albo o określonym czasie, albo by jego wykonanie powtarzało sie z określoną częstostliwością, albo połaczyć obie te możliwości: rozpoczęcie zadania w określonym czasie i powtarzanie go z określoną częstotliwością. Odwołanie zadania do wykonania lub zakończenie wykonującego się zadania (jeśli jest to zadanie powtarzające się) uzyskujemy za pomocą wywołania na jego rzecz metody cancel().

Obrazuje to poniższy przykładowy program.

import java.util.*;

// Klasa definiująca zadanie do wykonania
// tu będzie to wypisywanie komunikatów

class Message extends TimerTask {

  private String msg;        // komunikat
  private boolean showDate;  // czy pokazać datę

  public Message(String s, boolean show) {
    msg = s;
    showDate = show;
  }

  // Ten kod będzie wykonywany przez Timer
  // według charakterystyk czasowych podanych w metodzie schedule
  public void run() {
    String msg1 = msg;
    if (showDate) msg1 = "Jest " + new Date() + '\n' + msg;
    System.out.println(msg1);
  }
}

// Test
public class Timer1 {

  public static void main(String[] args) {

    // Utworzenie zadań
    Message msgTask1 = new Message("Przypominam o podlaniu kwiatów!", true),
            msgTask2 = new Message("I zakończ ten program!", false);

    // Czas rozpoczęcia drugiegi zadania:
    // za trzy sekundy od teraz
    long taskTime2 = System.currentTimeMillis() + 3000;

    // Utworzenie timera
    Timer tm = new Timer();

    // Zlecenie pierwszego zadania do wykonania
    // Ma się zacząć JUŻ - podano 0 jako moment startu
    // i powtarzać się co 2 sekundy
    tm.schedule(msgTask1, 0, 2000);

    // Zlecenie drugiego zadania do wykonania
    // Zacznie się w momencie określanym przez datę
    // - utworzony obiekt Date; tu - zgodnie z czasem timeTask2
    // ale mogłoby to być np. 11 lipca o 9 rano
    tm.schedule(msgTask2, new Date(taskTime2), 2000);

    // Po twierdzącej odpowiedzi na to pytanie ...
    int rc = javax.swing.JOptionPane.showConfirmDialog(null,
        "Czy kwiaty podlane?");
    // ... kończymy działanie zadania msgTask1
    // do czego służy metoda cancel():
    if (rc == 0) msgTask1.cancel();

    // zadanie 2 nadal działa
    javax.swing.JOptionPane.showMessageDialog(null, "Kończyć trzeba");

    // teraz kończymy wszystko
    System.exit(0);
  }

}
Działanie programu ilustruje rysunek.
r

Klasa Timer może okazać się bardzo użyteczna: dostarcza różnych strategii określania częstotliwości powtórzeń (np. czy - przy niedokładnym przecież pomiarze czasu środkami  programowymi - preferować zachowanie stałych odstępów czasowych między wykonaniem zadania czy też może dążyć do utrzymania sumarycznego czasu wykonania, wynikającego z liczby powtórzeń). Jest także dobrze skalowalna: działanie Timera jest takie samo przy małej jak i dużej (idącej w tysiące) liczbie kontrolowanych przez niego wątków-zadań,

4.11. Pułapki

Problem: samolubne i zagłodzone wątki

Co się stanie, jeśli w metodzie run(), definiującej działanie wątku zawrzemy pętlę wykonującą sie kilka milionów razy?
Czy inny wątek - w trakcie działania pętli - będzie miał szansę dostępu do procesora?
Może nie mieć, gdy ten długo wykonujący się wątek zawłaszczy czas procesora.
Wątek zawłaszczający nazywa się "asmolubnym", a ten, ktory nigdy nie ma szansy dostępu do procesora "zagłodzonym".
Możemy manipulować priorytetami, albo liczyć na systemowe wywłaszczanie... Ale to może nic nie dać (np. jeśli system nie podtrzymuje wywłaszczania).
Zagłodzenie jakiegoś wątku może pojawić się także na skutek określonej konfiguracji działania kilku nawet z pozoru bezpiecznych wątków.

Rozwiązanie

Zapewnić dobrowolne wywłaszczanie się wątków:
  
Problem:  zakleszczenie

Sytuacja: wątek A wywołuje na rzecz obiektu x synchronizowaną metodę mx. Obiekt x zostaje zaryglowany.
W tym samym czasie wątek B wywołuje na rzecz obiektu y synchronizowaną metodę my. Obiekt y zostaje zaryglowany.
W metodzie mx występuje wywołanie jakiejś innej metody sybchronizowanej na rzecz obiektu y. Ale obiekt jest zaryglowany i wątek A czeka na jego odryglowanie.
W metodzie my występuje wywołanie jakiejś metody synchronizowanej na rzecz obiektu x. Ale obiekt x jest zaryglowany i wątek B czeka.
Oba wątki będą czekać w nieskończoność.

Rozwiązanie
Odpowiednie projektowanie programów.
Wiele informacji na ten temat zawiera np. książka Gruźlewski, Weiss, "Programowanie współbieżne i rozproszone w przykładach i zadaniach", WNT 1993. 


4.10. Podsumowanie

Prgramowanie współbieżne jest trudną sztuką. Programy wielowątkowe mogą wykonywać się różnie na róznych platformach systemowych, a nawet na tej samej platformie, bowiem ich wykonanie zależy od nie dających się z góry przewidzieć momentów i kolejności otrzymywania i oddawania czasu procesora.
Każdy wątek może być odsunięty od procesora praktycznie w dowolnym miejscu wykonywanego kodu, w związku z czym współdziałanie wątków na tych samych obiektach może prowadzić do niespójnych stanów obiektów.

Najważniejsza zasada programowania współbieżnego i wielowątkowego, powtarzana przez najwybitniejszych specjalistów z tej dziedziny brzmi:



    Należy - jak tylko się da i kiedy tylko można- unikac porgramowania współbieżnego.



Jeśli zaś jesteśmy do tego zmuszeni, to należy zadbać o omówione w niniejszym wykładzie: