Wprowadzenie do programowania współbieżnego



Współbieżność oznacza równoległe wykonywanie różnych zadań. W ramach jednego programu efekt ten osiągany jest poprzez tworzenie i uruchamianie tzw. wątków. Poznamy zatem  istotę programowania wielowątkowego w Javie, sposoby tworzenia, uruchamiania, wstrzymywania i kończenia wątków oraz zagadnienia związane z synchronizacją wątków oraz ich koordynacją. Tematy te będą przedstawione na podstawowym, elementarnym poziomie. 


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

Uruchomienie dowolnego program (aplikacji) powoduje stworzenie  procesu w systemie operacyjnym. Dla każdego procesu alokowane są przez system wymagane przez niego zasoby np. pamięciowe i plikowe.

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 równoległe (teoretycznie) wykonywanie wielu procesów, z których każdy ma swój kontekst, w tym:  swoje zasoby.

W dalszym ciągu będziemy mówić wyłącznie o systemach wielozadaniowych i wielowątkowych. Należą do nich np. systemy Windows i OS/2, a w różnych wersjach systemów Unix i Linux wielowątkowość jest symulowana przez biblioteki wspomagające jądro systemu
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ą (teoretycznie) 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).


Równoległe działanie procesów w wielozadaniowych systemach operacyjnych obserwujemy niemal na co dzień.  Oto pisząc jakiś tekst w procesorze tekstów możemy równolegle kompilować program w Javie. Są to dwa różne procesy, związane z dwoma różnymi programami (procesorem tekstów i kompilatorem Javy).
Przestrzeń adresowa procesu - to zakres dostępnych dla procesu adresów pamięci operacyjnej. Zakres ten może obejmować wirtualnie całą pamięć operacyjną, ale dzięki systemowemu zarządzaniu pamięcią nie ma możliwości nakładania się na siebie fizycznej pamięci przydzielanej różnym procesom. Bardzo często przestrzenią adresową procesu określa się właśnie nie nakladające się na siebie dynamicznie przydzielane procesowi przez system obszary pamięci
Każdy z nich ma inną przestrzeń adresową. Każdy z nich -  po otwarciu jakichś plików (np. pliku z dokumentem i pliku z programem źródłowym) - zawiera deskryptory (inaczej zwane uchwytami) jednoznacznie identyfikujące otwarte pliki, które także stanowią przyklad zasobu przydzielonego procesowi.
Procesy mogą wspóldzielić przydzielone im zasoby, ale wymaga to specjalnych zabiegów programisty.

Bardzo łatwo możemy też zaobserwować równoległe działanie watków.  Np. w ramach programu wyszukiwania plików uruchamiany jest wątek przeszukujący katalogi, a jednocześnie interakcja użytkownika z interfejsem takiego programu nie jest zablokowana (kontroluje ją inny watek w ramach tego samego procesu - działającego programu wyszukiwania plików) i w każdej chwili możemy przerwać działanie wątku przeszukiwania, wybierając jakąś formę opcji "Stop".
Podobnie, ładując pliki z Internetu z poziomu przeglądarki WWW również widzimy dzialanie wątków. Przeglądarka "nie czeka" na załadowanie pliku - ładowaniem zajmuje się specjalnie uruchomiony wątek (w ramach procesu - przegladarki), a my możemy nadal korzystać z jej graficznego interfejsu.

Jak widzimy z wymienionych przykladów, 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.

Warto w tym miejscu zastanowić się co 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:
Jak wspomniano wcześniej, 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.


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

Przejdźmy teraz do praktyki posługiwania się wątkami w Javie.
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 (uwaga: nie należy mylić tej klasy z klasami Timer z pakietu javax.swing oraz Timer z pakietu java.util; tutaj pod tą nazwą opisujemy własną, prostą klasę - licznik czasu). Klasa ta 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);
     }
   }
}
Kodu zliczającego upływ czasu dostarczyliśmy w metodzie run(). Zmienna time jest licznikiem sekund. W pętli while usypiamy wątek, wykonujący tę metodę run(), na 1000 milisekund, czyli 1 sekundę (metoda sleep z klasy Thread), po czym zwiększamy licznik sekund (zmienna time) i wyprowadzamy informację o upływie czasu na konsolę.
W trakcie uśpienia wątek odsuwany jest od procesora (kod metody run nie wykonuje się wtedy).
Dzięki temu uzyskujemy pożądany efekt.
Metoda sleep może sygnalizować wyjątek InterruptedException, który powstaje na skutek przerwania działania wątku. 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();
  }
}
Tutaj w pętli pobieramy dane od użytkownika, dopóki nie wpisze on prawidłowo wszystkich znajdujących się w tablicy CAP stolic lub nie przerwie wpisywania, wybierając w dialogu Cancel.
Ponieważ wcześniej utworzyliśmy i uruchomiliśmy wątek zliczania czasu:
    Timer timer = new Timer();
    timer.start();
to 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.

Rozważmy modyfikację zadania o stolicach.
Zmodyfikować quiz o stolicach w  taki sposób, by kod programu był umieszczony w jednej klasie. W okienkach komunikatów pokazywać informację o aktualnym w  momencie otwarcia okienka stanie licznika czasu.  Przed zakończeniem programu podać w oknie komunikatów ile trwało wpisywanie stolic.


Możliwe rozwiązanie.
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();
  }
}

Zwrócmy uwagę: mamy tu również dwa wątki - odpytywania użytkownika oraz zliczania czasu. Ale kod wykonujący się w wątku zliczania czasu podaliśmy w metodzie run() w tej samej klasie, która odpowiada za odpytywanie użytkownika. Oba wątki dzielą między sobą ten sam obiekt - obiekt klasy Quiz1a utworzony w metodzie main i równolegle odwołują się do jego elementu, określonego przez zmienną time. Współdzielenie zmiennej uzyskaliśmy bez specjalnych dodatkowych zabiegów, ale takie równoległe sięganie do jednej zmiennej może powodować różne komplikacje. W tym przykładzie są one niewidoczne, ale dalej zobaczymy, że łatwość współdzielenia zasobów (tu: tego samego obiektu) w Javie okupiona jest - w większości przypadków -  koniecznością synchronizowania dostępu wątków do współdzielonych zasobów.

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();
Uwaga: tym razem w metodzie run() dla uśpienia wątku na sekundę używamy statycznej metody sleep z klasy Thread, która usypia bieżący wątek (w tym przypadku ten, w którym wykonuje się ta metoda run()). Nie mogliśmy użyć metody niestatycznej (this.sleep()), bo przecież nowa klasa Timer nie dziedziczy już klasy Thread, zatem wywołanie na rzecz jej obiektów metody sleep() z klasy Thread spowoduje błąd w kompilacji.

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

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

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.


Uwaga: istnieje jeszcze jeden sposób uruchamiania wątków, mianowicie użycie tzw. wykonawców (ExecutorService z pakietu java.util.concurrent).  Więcej na ten temat przy omawianiu pakietu java.util.concurrent (w następnym semestrze).


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
      }
    }
Oto wydruk testu, w którym - tak jak w końcu poprzedniego podpunktu - po trzech sekundach 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
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).
Kilka równolegle dzialającyh wątków zużycia paliwa będzie pustoszyć bak!
Możemy się o tym przekonać, modyfikując nieco kod w klasie Car, tak by wiedzieć jaki wątek zużywa paliwo i próbując np. trzy razy uruchomić ten sam samochód.

Wykorzystamy przy tym:
class Car {
    ...

    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.

Uwaga: jeśli wątek może być kontrolowane przez ExecutorService z pakietu java.util.concurrent, to właściwym sposobem kończenia pracy wątku jest użycie metody inetrrupt(), a w metodzie run(0 sprawdzanie flagi interrupted. Parę słów o tym w dalszych punktach, a więcej dowiemy się przy omawianiu pakietu java.util.concurrent.



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 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;
    // 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 szybko 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. Mogą tez być inen przyczyny zwolneinia rygla,  o których będzie mowa w podrozdziale o stanach wątków).

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 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.:
Jak widać, unikanie synchronizacji (przy zapewnieniu wielowątkowej spójności) może wymagać dużego wysiłku koncepcyjnego przy projektowaniu aplikacji i  wyborze sposobów kodowania. Są jednak też i pewne proste, niejako oczywiste zasady, których należy przestrzegać. Niewatpliwie synchronizacja nie ma żadnego sensu, jeśli wątki jedynie odczytują jakieś dane. Nie ma też powodów, aby synchronizować kody, o których wiadomo, że będą wykonywane tylko w jednym wątku.
W aplikacjach, które powinny działać naprawdę efektywnie (np. w czasie realnym) należy w ogóle unikać synchronizacji na poziomie metod.

Właśnie - po części w kontekście ew. możliwości poprawy efektywności działania programów, ale nie tylko - warto zwrócić uwagę na ogólniejszy (od metod synchronizowanych) sposób synchronizowania wątków - mianowicie - 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 (lub wątek ten nie zwolni rygla na skutek innych przyczyn, o których będzie mowa w podrozdziale o stanach wątków).

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.

Należy podkreślić, że konstruktory nie mogą być synchronizowane (nie możemy poprzedzić defnicji konstruktora słowem kluczowym synchronized). Konstrukcja synchronized(this) w konstruktorze także nie zadziała. Możliwym rozwiązaniem  synchronizacji kodu konstruktora jest wprowadzenie semafora w klasie i ryglowanie kodu na tym semaforze np.:

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
             }
         }


Kończąc omawianie synchronizacji warto poświęcić chwilę na pojęciowe, trochę terminologiczne, podsumowanie (jest to również istotne ze względu na różną terminologię stosowaną w literaturze).

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ą,  Przez synchronizację wątków  w Javie rozumiemy więc najczęściej  wzajemne wykluczanie wątkow w dostępie do obiektów. W przeciwieństwie do asynchronicznego, dowolnego w czasie, równoległego dostępu, synchronizacja oznacza sekwencyjny, kolejny w czasie dostęp wątków do zasobów. Slowo to jest również wygodne ze względu na łatwe kojarzenie ze słowem kluczowym synchronized.

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.

 



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:
 
Metoda wait() może mieć argument, który specyfikuje maksymalny czas oczekiwania. Po upływie tego czasu wątek zostanie odblokowany, niezależnie od tego czy użyto jakiegoś notify() wobec obiektu na którym było synchronizowane wait.


Spójrzmy na przykład  (schemat) 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()) i na tym obiekcie właśnie wątki będą synchronizowane.

Czy naprawdę oprócz sygnału notify potrzebny jest warunek "wartość gotowa"? W prostym przypadku wystarczyłoby być może samo notify.
Schemat pokazuje jednak ogólniejszą konstrukcję, kiedy samo notify (które może przyjść od różnych wątków) nie wystarcza.


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 (generowany 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 { // autor zastanawia się chwilę co napisać
        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();
   }

}

Warto skompilować program i prześledzić jego dzialanie.
Następnie, po usunięciu konstrukcji while(newTxt == ... ) oraz wait() i notifyAll() w metodach setTxtToWrite i getTxtToWrite skompilowac oraz uruchomić program ponownie i przekonac się, że sama synchronizacja wątków (pozostawiamy obie metody jako synchronizowane) nie wystarcza dla zapewienia własciwej kolejności działań.


Pojęcie monitora

W Javie z każdym obiektem oprócz rygla związana jest  "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".

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.).
Zatem wątki, będące w stanie Runnable można podzielić na aktualnie wykonujące się (będące w stanie Running) i aktualnie gotowe do wykonania (ale czekające na przydzielenie czasu procesora).

Wykonujący się wątek (będący w stanie Running) może dobrowolnie oddać czas procesora za pomocą wywolania metody yield() . Przy tym nie następuje jednak zwolnienie rygla. Wątek pozostaje w stanie Runnable, ale czeka na ponowne przydzielenie czasu procesora.

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

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"). 
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");
          System.out.println("Co powie isInterrupted() ?: "+ isInterrupted());
      }
      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();
    }
  }

}

public 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);
  }
}
Rysunki pokazują działanie programu: wubór opcji I (przerwanie wątku), po której wątek nadal działa, natomiast po następującym po tym wyborze S (suspend-wstrzymanie wątku) sygnalizowany jest wyjątek InterruptedException (o czym świadczy widoczny komunkat), wątek jest jednak wstrzymany,  bowiem w obsłudze wyjątku nie zmieniamy tego stanu. 

r

r

Warto zauważyć, że flaga przerwania wątku jest już "zdjęta", a komunikat mówiący o tym, że wątek jest przerwany - błędny. Dzieje się tak dlatego, że wykonanie metody isInterrupted() w wątku głównym nie jest dobrze zsynchronizowane.  Jako ćwiczenie można zaproponować modyfikację przdstawionego kodu w taki sposób, by komunikat pokazywany w okienku był zgodny z komunikatem wypisywany na konsoli przy obsłudze wyjątku InterruptedException.



8. Priorytety wątków


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.

Przypomnijmy: 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().   




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 programch 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





9. Demony i grupy

Niejaki dla porządku (bo nie są to zagadnienia pierwszoplanowe) kilka słów należy powiedzieć o specjalnym rodzaju wątków - demonach, oraz o łączeniu wątków w 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,

Nieraz mogliśmy zaobserwować w przykładach niniejszej książki, iż program musiał być kończony przez wywołanie metody exit. Działo się tak w sytuacjach gdy używaliśmy dialogów JOptionPane. Przyczyna: w sytuacjach, gdy odwołujemy się do klas GUI (AWT i Swing), oprócz głównego wątku uruchamiany jest wątek obsługi zdarzeń. To on właśnie - jako zwykły, nie demon - blokuje zakończenie programu.

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.


10. Zliczanie czasu i zadania czasowe


Dotąd w tym rozdziale - przy okazji wielowątkowych programów przykładowych - dostarczaliśmy własnych sposobów na zliczanie czasu. Za  każdym razem wymagało to pisania jakiegoś kodu. Tymczasem w standardzie Javy istnieją sposoby już gotowe. Jeden z nich - w postaci użycia kalsy javax.swing.Timer - poznaliśmy już przy omawianiu klas wewnętrznych. Jest też drugi sposób, bardziej elastyczny od "zegara" swingowego - mianowicie użycie klas Timer i TimerTask z pakietu java.util.
W tym podrozdziale zasygnalizujemy tylko ich istnienie, po szczególy odsyłając do dokumentacji.

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ń,

11. Pułapki programowania wspólbieżnego

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.
Informacje na ten temat zawiera np. książka Gruźlewski, Weiss, "Programowanie współbieżne i rozproszone w przykładach i zadaniach", WNT 1993.