<

9. Współbieżna Java: synchronizacja i koordynacja


Zajmiemy się teraz nowymi - dostarczanymi przez pakiet java.util.concurrent - machanizmami synchronizacji i koordynacji watków.  Znacznie poszerzają one możliwości współbieżnego programowania. Wprowadzają też kilka istotnych ułatwień.


1. Przypomnienie: synchronizacja i koordynacja działania wątków

Synchronizacja jest mechanizmem, który zapewnia, że kilka wykonujących się wątków nie będzie równocześnie wykonywać tego samego kodu, w szczególności - działać na tym samym obiekcie.


Synchronizacje jest potrzebna po to, by współdzielenie zasobu przez kilka wątków nie prowadziło do niespójnych stanów zasobu.

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 wątpliwoś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

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ższa animacja.





Komentarze:
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.

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ę synchronizację.

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 synchronizatorami lub muteksami (od ang. mutual-exclusion semaphore). W Javie rolę synchronizatorów pełnią rygle (ang lock).

Do wersji 1.5 synchronizację wątków można było uzyskać wyłącznie za pomocą słowa kluczowego synchronized.

Sposoby użycia.

Synchronizowane mogą być metody i bloki.


Metoda synchronizowana oznaczana jest w deklaracji słowem synchronized:

        synchronized void metoda() {
        ...
        }



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


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ć inne przyczyny zwolnienia rygla,  o których będzie mowa w podrozdziale o stanach wątków).

Z kolei 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 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 Dlatego mowa o synchronizacji. 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.

2. Przypomnienie: 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 w Javie do wersji 1.5  uzyskiwało się za pomocą metod klasy Object:

W tej konwencji 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) 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óćmy 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 (są to kolejne elementy 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 działanie.
Następnie, po usunięciu konstrukcji while(newTxt == ... ) oraz wait() i notifyAll() w metodach setTxtToWrite i getTxtToWrite skompilowac oraz uruchomić program ponownie i przekonać się, że sama synchronizacja wątków (pozostawiamy obie metody jako synchronizowane) nie wystarcza dla zapewnienia właściwej 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óżnieniu 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".



3. Rygle jako obiekty typu Lock

W Javie 1.5 oprócz wcześniej dostępnych środków synchronizacji wątków - pojawiły się nowe.

Teraz rygiel może być obiektem klasy implementującej interfejs Lock. W pakiecie java.util.concurrent mamy dwie takie klasy:
Nowe rygle wydają się na pierwszy rzut oka bardziej przejrzyste niż użycie synchronizowanych metod, operujemy bowiem w sposób jawny na obiektach-ryglach, a nie na domyślnie (i niewidocznie) skojarzonych z obiektami ich ryglach. 
Mają one i inne zalety:

Podstawowy schemat działania.

Lock lock = new ReentrantLock();  // utworzenie rygla
// ....
// KOD WYKONYWANY WSPÓŁBIEŻNIE
lock.lock();                      // zamknięcie rygla (1)
// ... kod sekcji krytycznej
lock.unlock();                    // zwolnienie rygla (2)
Niech w jakimś wątku wykonywana jest instrukcja (1) (lock.lock()). Rygiel jest zamykany i wykonywany jest kod sekcji krytycznej. Inne wątki, które chcą wejść w ten kod (wykonać instrukcję lock.lock()) będą blokowane na tym wywołaniu dopóki właściciel rygla (ten kto go zamknął) nie dobiegnie do końca sekcji krytycznej i nie zwolni rygla (lock.unlock()).
W ten sposób sekcja krytyczna może być wykonywana od początku do końca tylko w jednym wątku.

Są tu pewne podobieństwa do działania bloków synchronizowanych, ale też i znaczące różnice.

Przede wszystkim, jeżeli w kodzie sekcji krytycznej wystąpi wyjątek, to blokada rygla może nie zostać zwolniona i inne wątki pozostaną na tym ryglu zablokowane na zawsze. Jest to błąd trudny do wykrycia.
Zobaczmy:
import java.util.concurrent.*;
import java.util.concurrent.locks.*;

class StringTab {

  private String[] txt;
  private Lock lock = new ReentrantLock();


  public StringTab(String[] txt) {
    this.txt = txt;
  }

  public void set(int i, String s) {
    lock.lock();
    txt[i] = s;
    lock.unlock();
  }

  public String get(int i) {
    String t = null;
    lock.lock();
    t = txt[i];
    lock.unlock();
    return t;
  }

}


public class Test1 {
  public static void main(String[] args) {

    final StringTab st = new StringTab(new String[] { "ala", "kot", "pies" });
    new Thread( new Runnable() {
      public void run() {
        st.set(3, "tygrys");
      }
    }). start();

    new Thread( new Runnable() {
      public void run() {
        try {
          TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        System.out.println("Minęły 2 sek. - Wątek 2 działa");
        System.out.println(st.get(0));
        System.out.println("Wątek 2 się kończy");
      }
    }). start();
  }
}
Wynik działania programu:


Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
    at locksIntro.StringTab.set(Test1.java:18)
    at locksIntro.Test1$1.run(Test1.java:39)
    at java.lang.Thread.run(Unknown Source)
Minęły 2 sek. - Wątek 2 działa


W tym programie zastosowaliśmy synchronizację w dostępie do wspólnego zasobu - tablicy Stringów. Słusznie, ponieważ kody metod get i set z klasy StringTab mogą być wykonywane współbieżnie przez wiele wątków.
Jednak kod wykonywany w jednym z wątków (ustalający wartość elementu tablicy o indeksie 3) spowodował wyjątek ArrayIndexOutOfBoundsException (nie ma indeksu 3!) i kod ten zostaje przerwany przed zwolnieniem rygla. Drugi wątek już wystartował, ale czeka sobie 2 sekundy (proszę zwrócić uwagę na możliwość użycia nowej formy wywołania metody sleep()). Po tych dwóch sekundach wyprowadza komunikat i próbuje wykonać metodę get. Metoda ta - jak widać - próbuje zamknąć rygiel (dostęp do sekcji krytycznej), ale ten już jest zamknięty przez pierwszy wątek. Wątek drugi będzie wiecznie zablokowany na tym ryglu, bowiem jego właściciel (ten kto go zamknął) czyli pierwszy wątek już dawno zginął i nie może go otworzyć.

Błędy mogą być subtelne. Wydaje się oczywistością, że w metodach set i get powinniśmy się bronić przed wyjątkiem ArrayIndexOutOfBoundsException. Oczywistym sposobem jest przekazanie obsługi do wywołującego (czyli dodanie w sygnaturach klauzuli throws ...). To oczywiście nie pomoże, bo rygiel pozostanie zamknięty. A może obsłużyć wyjątek wewnątrz metody? Np.
 public void set(int i, String s) {
    lock.lock();
    try {
      txt[i] = s;
    } catch (ArrayIndexOutOfBoundsException exc) {}
    lock.unlock();
  }
To rozwiąże nam problem tego konkretnego programu (będzie działał dobrze). Ale nie jest uniwersalnym rozwiązaniem, ani nawet dobrym. Po pierwsze nawet tu mogą się pojawiać inne wyjątki (np. NullPointerException, gdy przekażemy w konstruktorze jako referencję do tablicy null). Po drugie, metody mogą być bardziej skomplikowane, wołać inne i tam może się pojawić jakiś wyjątek klasy RuntimeException. Po trzecie wreszcie, decydowanie o tym co zrobić gdy podano niewłaściwiy argument (tu wadliwy indeks) należy zawsze pozostawić wołającemu (czyli jednak throws... lub sygnalizacja wyjątku typu RuntimeException ).


4. Konieczność użycia klauzuli finally. Klasa ReentrantLock.


Elegenckim rozwiązaniem opisanego wyżej problemu jest użycie bloku try i klauzuli finally.

  public void set(int i, String s) {
    lock.lock();
    try {
      txt[i] = s;
     } finally {
       lock.unlock();
     }
  }

Takiej konstrukcji powinniśmy używać zawsze, nawet jeśli nie liczymy się z wystąpieniem jakichkolwiek wyjątków.


Ma ona bowiem znaczenie nie tylko wtedy, gdy ew. wyjątki mogą wystąpić, ale również jest jedynym sposobem poprawnego oprogramowanie metod, które zwracają wynik.

Oto może się wydać, że konstrukcja:
Lock lock = new ReentrantLock();

public typ metoda() {
  typ zmienna;
  lock();
  // ...
  unlock();
  return zmienna;
}  
jest równoważna:
Object mutex = new Object();

public typ metoda() {
  typ zmienna;
  synchronized(mutex) {
    // ...
    return zmienna;
  }
}  
Nic błędniejszego.
Zobaczmy na znanym nam już przykładzie, w którym wielokrotne wywołanie w pętli przez różne wątki metody balance() daje nieoczekiwany wynik (różny od 0), jesli nie ma synchronizacji. 
class Balance {

  private int number = 0;
  private Lock lock = new ReentrantLock();

  public int balance() {
    lock.lock();
    number++;
    System.out.print("*");
    number--;
    lock.unlock();
    return number;
  }

}

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;
      for (int i = 0; i < count; i++) {
        wynik = b.balance();
        if (wynik != 0) break;
      }

    System.out.println("\n"+ Thread.currentThread().getName() +
                       " konczy z wynikiem  " + wynik);
  }
}

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];

    long start = System.nanoTime();

    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("Czas: " + (System.nanoTime() - start) );


  }


}       
Niby jest synchronizacja, a jednak niektóre wątki kończą z wynikiem różnym od 0, np.


W42 konczy z wynikiem  0
*
W39 konczy z wynikiem  1


Okazuje się, że return znajdujące się poza sekcją krytyczną może zwrócić wynik, który produkuje już inny wątek: Wygląda to tak: wątek A zamknął rygiel wykonał obliczenia, otworzył rygiel i w tym momencie został wywłaszczony. Wątek B rozpoczyna obliczenia, w momecie wypisywania gwiazdki (blokowanie na we/wy) jest wywłaszczany, a wątek A wraca do procesora i zwraca wartość zmiennej number, którą przed chwilą ustalił wątek A.

A zatem koniecznie należy użyć konstrukcji try - finally, chociaż w tym przykładzie nie ma żadnej możliwości pojawienia się wyjątków.

  public int balance() {
    try {
      lock.lock();
      number++;
      System.out.print("*");
      number--;
      return number;
    } finally {
        lock.unlock();
    }
  }
W tym przypadku wykonanie return zostanie wstrzymane do chwili zakończenia bloku finally i uzyskamy właściwe wyniki.

Tak samo powinniśmy poprawić metodę get(...) z klasy StringTab.
Zamiast niebezpiecznego:
  public String get(int i) {
    String t = null;
    lock.lock();
    t = txt[i];
    lock.unlock();
    return t;
  }
należy napisać:
  public String get(int i) {
    lock.lock();
    try {
      return txt[i];
    } finally {
        lock.unlock();
    }
  }

Należy podkreślić, że zastosowanie try/finally musi być w pełni świadome, bo obarczone jest dodatkowymi niebezpieczeństwami.

Ilustacją  pierwszego przypadku może być zmodyfikowany kod przykładu z bilansowaniem liczby.
Niech np. oprócz dodawania i odejmowania wykonywane są jeszcze jakieś inne operacje:
 int w;
 int innaLiczba;
 // ....

 // Kod metody balance  
 lock.lock();
 try {

      number++;
      if (print) System.out.print("*");
      w = number/innaLiczba;
      number--;

      return number;
  } finally {
      lock.unlock();
  } 
Jesli innaLiczba = 0, to powstanie wyjątek ArithmeticException, blok try zostanie przerwany, w finally zwolniony rygiel, ale stan obiektu Balance będzie niespójny (założenie - po wykonaniu metody balance ma to być 0). Inne wątki, które opierają się na tym założeniu mogą prowadzić do dalszych złych (coraz gorszych) wyników.

Przykładowy wynik działania kilku wątków wykonujących kod:
 public void run() {
    int wynik = 0;
      for (int i = 0; i < count; i++) {
        boolean print;
        if (i%20 == 0) print = true;
        else print = false;
        try {
          wynik = b.balance(print);
        } catch(Exception exc) { }
        if (wynik != 0) break;
      }
    System.out.println("\n"+ Thread.currentThread().getName() +
                       " konczy z wynikiem  " + wynik);
    System.out.println("Stan b: " + b);
  }
}  

*****
L1 konczy z wynikiem  0
Stan b: 100
*****
L2 konczy z wynikiem  0
Stan b: 200
*****
L3 konczy z wynikiem  0
Stan b: 300
*****
L4 konczy z wynikiem  0
Stan b: 400
*****
L5 konczy z wynikiem  0
Stan b: 500
*****
L6 konczy z wynikiem  0
Stan b: 600
*****
L7 konczy z wynikiem  0
Stan b: 700
*****
L8 konczy z wynikiem  0
Stan b: 800
*****
L9 konczy z wynikiem  0
Stan b: 900
*****
L10 konczy z wynikiem  0
Stan b: 1000


Warto przy okazji zwrócić uwagę, że działanie metody balance() jest przerywane jeszcze przed zwrotem wyniku, a to co pokazuje "wynik" wątku jest inicjalną wartością nadaną w metodzie run(). Faktyczny stan obiektu b jest - jak widać zupełnie inny.

Może więc (nawet w tym prostym przypadku) trzeba napisać tak:

public int balance1() {
    lock.lock();
    boolean balanced = true;
    try {
      number++;
      balanced = false;
      w = number/innaLiczba;
      number--;
      balanced = true;
      return number;
    } finally {
        if (!balanced) number--;
        lock.unlock();
    }

  }
Drugi przykład dotyczy przerywania blokowania na ryglu.
Mamy oto jakiś wątek, który zamyka rygiel w sposób przerywalny.
 Lock lock = new ReentrantLock();

  Runnable task1 = new Runnable() {
     public void run() {
       System.out.println("Task 1 begins");
       try {
         lock.tryLock(10, TimeUnit.SECONDS); // albo lock.lockInterruptibly();
         System.out.println("Task 1 entered");
       } catch(InterruptedException exc) {
           System.out.println("Task 1 interrupted");
       } finally {
           lock.unlock();
       }
       System.out.println("Task 1 stopped");
     }
  };
Jeśli po uruchomieniu tego zadania zostanie ono zablokowane na ryglu i przerwiemy go (posyłając do odpowiedniego wątku interrupt() (bezpośrednio lub za pośrednictwem ExecutorService), to uzyskamy taki oto obrazek:

Task 1 begins
Task 1 interrupted
Exception in thread "pool-1-thread-2" java.lang.IllegalMonitorStateException
....


Aby uniknąć takich niespodzianek zwalniając rygiel w finally winniśmy sprawdzić, czy nadal przynależy on do bieżącego wątku, stosując metodę isHeldByCurrentThread().

 Lock lock = new ReentrantLock();

  Runnable task1 = new Runnable() {
     public void run() {
       System.out.println("Task 1 begins");
       try {
         lock.tryLock(10, TimeUnit.SECONDS); // albo lock.lockInterruptibly();
         System.out.println("Task 1 entered");
       } catch(InterruptedException exc) {
           System.out.println("Task 1 interrupted");
       } finally {
           ReentrantLock l = (ReentrantLock) lock;
           if (l.isHeldByCurrentThread()) lock.unlock();
       }
       System.out.println("Task 1 stopped");
     }
  };
Ta metoda nie jest metodą interfejsu Lock, ale klasy go implementującej - ReentrantLock.

Reentrant (wielobieżny) oznacza możliwość ponownego wykonania tej samej operacji przez ten sam wątek w sytuacji, gdy jest ona w trakcie wykonywania w tym wątku. W przypadku rygli chodzi o możliwość ponownego wprowadzania tej samej sekcji krytycznych (ponownego zamykania rygla) przez wątek, który już ten rygiel zamknął


A więc rygiel może być zamykany wielokrotnie przez ten sam wątek (właściciel rygla nie czeka na zamkniętym przez siebie ryglu, inne wątki czekają). Zliczana jest liczba zamknięć (możemy się dowiedzieć jaka ona jest za pomocą metody int getHoldCount()). Każde otwarcie rygla (unlock()) zmniejsza tę liczbę. Dopiero gdy jest równa 0 - inne wątki mogą uzyskać dostęp do sekcji krytycznej.

5. Idiomy ryglowania


Ostateczenie można przedstawić kilka idiomatycznych, właściwych sposóbów postępowania z ryglami typu Lock.

Sekcja krytyczna z nieprzerywalną blokadą

Lock lock = new ReentrantLock();
....

lock.lock();
try {
   // ...
| finally {
   // ... zapewnienie spójności stanów obiektu
   lock.unlock();
}  
 
Sekcja krytyczna z przerywalną blokadą

Lock lock = new ReentrantLock();
....
try {
  lock.lockInterruptibly();

   // ...
| catch(InterruptedException exc) {
   // ... przerwanie działania - zakończenie metody run
finally {
   // ... zapewnienie spójności stanów obiektu
   ReentrantLock l = (ReentrantLock) lock;
   if (l.isHeldByCurrentThread()) lock.unlock();
}  

6. Efektywność synchronizacji

Warto zauważyć, że wprowadzenie println do  kodu metody balance() w klasie Balance (z poprzednich przykładów) powoduje silną konkurencję wątków.
W sytuacji silnej konkurencji (ale tylko wtedy!) użycie bezpośrednich rygli jest bardziej efektywne od użycia synchronized.
Aby to sprawdzić możemy zastosować następujący - modyfikujący poprzedni przykład - program testowy:
class Balance {

  private int number = 0;
  private Lock lock = new ReentrantLock();


  private boolean useLock;   // której metody synchronizacji użyć?

  public Balance(boolean ul) {
    useLock = ul;
  }

  public int balance(boolean print) { // parametr print mówi czy wypisać gwiazdkę
    if (!useLock) return balanceSynchro(print);
    else return balanceLocked(print);
  }

  // Synchronizacja za pomocą bezposrednich rygli
  public int balanceLocked(boolean print) {
    try {
      lock.lock();
      number++;
      if (print) System.out.print("*");
      number--;
      return number;
    } finally {
        lock.unlock();
    }
  }

  // Użycie synchronized
  public synchronized int  balanceSynchro(boolean print) {
    number++;
    if (print) System.out.print("*");
    number--;
    return number;
  }


}

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;
      for (int i = 0; i < count; i++) {
        boolean print;
        if (i%20 == 0) print = true;  // gwiazdkę będziemy drukować co 20 iterację
        else print = false;
        wynik = b.balance(print);
        if (wynik != 0) break;
      }


    System.out.println("\n"+ Thread.currentThread().getName() +
                       " konczy z wynikiem  " + wynik);
  }
}

class BalanceTest {

  static ArrayList<String> czasy = new ArrayList<String>(); // wyniki czasowe

  public static void test(int tnum, boolean lock) {

    // jesli lock jest true będziemy używać bezp. rygli
    Balance b = new Balance(lock);
    String wynik = lock ? "Lock " : "Synchro ";
    String id = lock ? "L" : "S";
    wynik += tnum;
    // Tworzymy i uruchamiamy wątki
    Thread[] thread = new Thread[tnum];

    long start = System.currentTimeMillis();

    for (int i = 0; i < tnum; i++)
      thread[i] = new BalanceThread(id +(i+1), b, 100);

    // 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);
    }
    wynik += " Czas: " + (System.currentTimeMillis() - start);
    System.out.println(wynik);
    czasy.add(wynik);
    // Uduwamy niepotrzebne obiekty-wątki
    for (int i = 0; i < thread.length; i++) { thread[i] = null; }
    System.gc();
  }

  public static void main(String[] args) {
    //Test synchro
    for (int i=100; i<=1000; i+=100) {
      test(i, false);
    }
    // Test Lock
    for (int i=100; i<=1000; i+=100) {
      test(i, true);
    }
    for (String msg : czasy) { System.out.println(msg);

    }

  }


} 
Wyniki testu prezentuje poniższy rysunek.

r

Należy jednak pamiętać, że:
Zatem nie względy efektywności powinny decydować o wyborze mechanizmu podstawowej synchronizacji.

7. Lock czy synchronized?

Użycie synchronized jest w wielu przypadkach łatwiejsze (mniej pisania kodu) i pozwala uniknąć subtelnych pułapek.
Jeśli więć nie potrzebujemy w naszym kodzie dodatkowcyh możliwości zapewnainych przez bezposrednie rygle typu Lock - używajmy tradycyjnego synchronizowania przez synchronized.


Użycie bezpośrednich rygli (typu Lock) wymaga trochę więcej kodowania i uwagi.
Ale opłaca się w przypadkach kiedy:
Lock lock;

boolean got = lock.tryLock();
if (got) {
   try {
     // sekcja krytyczna
   } finally {
       lock.unlock();
   }  
} else {
    robimy coś innego - może staramy się np. przetwarzać inny zasób
}
Ten ostatni przypadek ilustruje znany nam przykład aplikacji WEB (zob. wykład o wzorcu MVC). W tamtym kodzie za synchronizowaliśmy wykonanie Command. Trzeba jednak rozciągnąć tę synchronizację na serwlet prezentacji i pobierania paremtrów. Uzycie synchronized nie pozwala na to, bo są to strukturalnie inne fraghmenty kodu (umieszczone w innych blokach). Tylko zastosowanie bezposrednich rygli typu Lock pomoże rozwiązać problem.

Oto zmodyfikowany kod.
  // Obsługa zleceń
  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {

    resp.setContentType("text/html");

    // Wywolanie serwletu pobierania parametrów
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Pobranie bieżącej sesji
    // i z jej atrybutów - wartości parametrów
    // ustalonych przez servlet pobierania parametrów
    // Różne informacje o aplikacji (np. nazwy parametrów)
    // są wygodnie dostępne poprzez własną klasę BundleInfo

    HttpSession ses = req.getSession();

    String[] pnames = BundleInfo.getCommandParamNames();
    for (int i=0; i<pnames.length; i++) {

      String pval = (String) ses.getAttribute("param_"+pnames[i]);

      if (pval == null) return;  // jeszcze nie ma parametrów

      // Ustalenie tych parametrów dla Command
      command.setParameter(pnames[i], pval);
    }

    // Wykonanie działań definiowanych przez Command
    // i pobranie wyników
    // Ponieważ do serwletu może naraz odwoływać sie wielu klientów
    // (wiele watków) - potrzebna jest synchronizacja

    Lock mainLock = new ReentrantLock();

    mainLock.lock();
      // wykonanie
      command.execute();

      // pobranie wyników
      List results = (List) command.getResults();

      // Pobranie i zapamiętanie kodu wyniku (dla servletu prezentacji)
      ses.setAttribute("StatusCode", new Integer(command.getStatusCode()));

      // Wyniki - będą dostępne jako atrybut sesji
      ses.setAttribute("Results", results);
      ses.setAttribute("Lock", mainLock);


    // Wywołanie serwletu prezentacji
    disp = context.getRequestDispatcher(presentationServ);
    disp.forward(req, resp);
  }

Zmknięty rygiel oywieramy dopiero w serwlecie prezentacji:
public class ResultPresent extends HttpServlet {


  public void serviceRequest(HttpServletRequest req,
                             HttpServletResponse resp)
                             throws ServletException, IOException
  {
    ServletContext context = getServletContext();

    // Włączenie strony generowanej przez serwlet pobierania parametrów
    // (formularz)
    String getParamsServ = context.getInitParameter("getParamsServ");
    RequestDispatcher disp = context.getRequestDispatcher(getParamsServ);
    disp.include(req,resp);

    // Uzyskanie wyników i wyprowadzenie ich
    // Controller po wykonaniu Command zapisał w atrybutach sesji
    // - referencje do listy wyników jako atrybut "Results"
    // - wartośc kodu wyniku wykonania jako atrybut "StatusCode"

    HttpSession ses = req.getSession();
    Lock mainLock = (Lock) ses.getAttribute("Lock");

    List results = (List) ses.getAttribute("Results");
    Integer code = (Integer) ses.getAttribute("StatusCode");

    mainLock.unlock();  // otwarcie rygla


    PrintWriter out = resp.getWriter();
    out.println("<hr>");

    // Uzyskanie napisu właściwego dla danego "statusCode"
    String msg = BundleInfo.getStatusMsg()[code.intValue()];
    out.println("<h2>" + msg + " aha!" + "</h2>");

    // Elementy danych wyjściowych (wyników) mogą być
    // poprzedzane jakimiś opisami (zdefiniowanymi w ResourceBundle)
    String[] dopiski = BundleInfo.getResultDescr();

    // Generujemy raport z wyników
    out.println("<ul>");
    for (Iterator iter = results.iterator(); iter.hasNext(); ) {
      out.println("<li>");

      int dlen = dopiski.length;  // długość tablicy dopisków
      Object res = iter.next();
      if (res.getClass().isArray()) {  // jezeli element wyniku jest tablicą
        Object[] res1 = (Object[]) res;
        int i;
        for (i=0; i < res1.length; i++) {
          String dopisek = (i < dlen ? dopiski[i] + " " : "");
          out.print(dopisek + res1[i] + " ");
        }
        if (dlen > res1.length) out.println(dopiski[i]);
      }
      else {                                      // może nie być tablicą
        if (dlen > 0) out.print(dopiski[0] + " ");
        out.print(res);
        if (dlen > 1) out.println(" " + dopiski[1]);
      }
      out.println("</li>");
    }
    out.println("</ul>");
  }
//..
}


8.  Dodatkowe możliwości rygli


Uczciwe rygle

Lock lock = new ReentrantLock(true);  // fairness = true

Najdłużej czekający będą mieli wcześniej dostęp. Koszt efektywnościowy.

Read-Write Locks

Wiele wątków czyta, modyfikacje są rzadsze. Efektywność.

Sposób postępowania:

A. Utworzenie obiektu - rygla
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

B. Pobranie read/write locków
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

C. Użycie readLocków wszędzie tam gdzie odczytujemy zasoby

public double getData() {
  readLock.lock();
  try { . . . }
  finally { readLock.unlock(); }
}

D. Użycie writeLocków wszędzie tam, gdzie modyfikujemy zasoby.

public void modifyData(. . .)  {
  writeLock.lock();
  try { . . . }
  finally { writeLock.unlock(); }
}



9. Warunki

Pakiet java.util.concurrent dostarcza alternatywnego wobec wait-notify  mechanizmu koordynacji dzialania wątków.
Wprowadzany jest on przez klasę Condition.


Lock lock;

Condition cond1 = lock.newCondition();   // stworzenie warunku w kontekście lock

lock.lock();
//....
try {
    cond.await();  // rygiel jest otwierany, wątek przechodzi do stanu WAITING
}                       // i wraca do stanu RUNNABLE z zamkniętym ponownie ryglem, gdy wystąpi jedno ze zdarzeń
lock.unllock();     //  - inny wątek wywołał signal lub signalAll
                         //  - wystąpił wyjątek InterruptedException
                         // - spurious wakeup (mechanizmy systemowe przerwały wait)

Inny wątek:
lock.lock()
cond.signal() lub cond.signalAll();
lock.unlock();


Różnice wobec wait i notify:


Uwaga na możliwość "spurious wakeup" - konieczność dodatkowych warunków i sprawdzania ich w pętli.

Przykład (modyfikacja kodu Author-Writer):

import java.util.concurrent.locks.*;

class Teksty {

  Lock lock = new ReentrantLock();
  Condition txtWritten = lock.newCondition();
  Condition txtSupplied = lock.newCondition();

  String txt = null;
  boolean newTxt = false;

  // Metoda ustalająca tekst - wywołuje Autor
  void setTextToWrite(String s) {
    lock.lock();
    try {
      if (txt != null) {
        while (newTxt == true)
          txtWritten.await();
      }
      txt = s;
      newTxt = true;
      txtSupplied.signal();
    } catch (InterruptedException exc) {
    } finally {
         lock.unlock();
    }
  }


  // Metoda pobrania tekstu - wywołuje Writer
   String getTextToWrite() {
     lock.lock();
     try {
       while (newTxt == false)
         txtSupplied.await(); // może być Interrupted
       newTxt = false;
       txtWritten.signal();
       return txt;
     } catch (InterruptedException exc) {
         return null;
     } finally {
         lock.unlock();
     }
   }


}

// 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();
     try { Thread.sleep(3000); } catch(Exception exc) {}
     t2.start();
   }

}


Klasa Condition ma i inne  bogate możliwości. Oto ilustrujący je zestaw metod.

 void await()
          Bieżący wątek jest wstrzymywany dopóki nie otrzyma sygnału lub nie zostanie przerwany (interrupt).
 boolean await(long time, TimeUnit unit)
          Bieżący watek jest wstrzymywany dopóki nie otrzyma sygnału lub nie zostanie przerwany (interrupt), albo nie minie podany czas.
 long awaitNanos(long nanosTimeout)
          j.w..
 void awaitUninterruptibly()
          Tu nie ma możliwości przerwania oczekiwania przez interrupt().
 boolean awaitUntil(Date deadline)
          Oczekiwanie dopóki nei ma signal, interrupt() lub nie minie podana data.
 void signal()
          Budzi jedne z czakjacych na warunku wątków.
 void signalAll()
          Budzi wsyztskie czekające watki.



10. Synchronizatory wyższego poziomu, blokujące kolejki, konkurencyjne kolekcje i atomiki.


Pakiet java.util.concurrent udostępnia nowe synchronizatory wyższego poziomu:


a także blokujące kolejki (ulatwiające pisanie programów wymagających koordynacji wątków):


Do zestwu kolekcji dodano również kolekcje przygotowane na efektywne działanie w sytuacji dużej konkurencji:


Pakiet java.util.concurrent.atomic daje natomiast możliwości wielowątkowo bezpiecznego dzialania na zmiennych typów prostych bez użcycia synchronizaji. Zagwarantowano to atomistycznośc operacji modyfikacji zmiennych i dzięki temu można unikac czasowo ksoztownej synchronizaji.

Proszę zapoznać się z dokumentacją przedstawionych klas.

11. Podsumowanie

Zapoznaliśmy się z nowymi mechanizmami synchronizacji i korrdynacji dzialania wątków.
W szczególności:


12. Zadania

Zadanie 1

Mamy n zasobów, które są modyfikowane przez m wątków. Modyfikacje trochę trwają (użyć sleep).
Sprawdzić czy wątek odczytujący dane z zasobów będzie działał bardziej efektywnie, jeśli zastosować tryLock.


Zadanie 2

Pokazać zastosowanie read/write locków i porównać ich efektywność w stosunku do zwykłej synchronizacji.

Zadanie 3

Napisać program Author-Writer przy użyciu blokujących kolejek.

Zadanie 4

Porównać efektywnośc współbieżnego dzialanai na zwykłej mapie ( HshMap) i na mapie klasy ConcurrentHashMap.