Javowy framerwok do obsługi współbieżności to nie tylko dziedziczenie po interfejsie theread, ale również wiele innych przydatnych klas, interfejsów i innych rozwiązań. Niektóre z nich opisałem w poprzednich artykułach. Dzisiaj na zakończenie postaram się opisać pozostałe, nie omówię tu oczywiście wszystkich. To więc startujemy:

Semaphore i Lock

Standartowo w Javie metody, fragmenty kodu synchronizujemy przy pomocy słowa kluczowego synchronized. Wtedy to jest zakładany systemowy monitor. Monitor jest to specjalny obiekt, do którego dostęp w danej chwili może mieć tylko jeden wątek. Oprócz standardowego bloku synchronizowanego możemy zastosować semafory i locki.

  • Semafor – kontroluje dostęp do obiektu za pomocą licznika, który określa pewną pulę pozwoleń na dostęp. Jeżeli licznik ma wartość większą od zera to jest przydzielany dostęp, jeżeli wartość „zero” to następuje odmowa. Semafory zapewniają największą swobodę działania, lecz ich dobra implementacja wymaga największego nakładu pracy. Musimy sami zapewnić odpowiednią kolejność zakładania i ściągania blokad. Możemy ich użyć wtedy, kiedy wiemy, że dany zasób zapewnia pewną ilość wolnych połączeń, a nie chcemy ich przekroczyć.
  • Lock – mechanizm który łączy ze sobą działanie semafora i monitora. Ma większą elastyczność od monitora, a jednoocznie mniej kłopotliwy w implementacji od semafora.
    W jdk 1.8 dyspozycji mamy dwie wersje Lock:

    • ReentrantLock – standardowy mechanizm blokujący w przypadku odczytu i zapisu
    • ReentrantReadWriteLock – stosuje synchronziację typu read/write locks. Pozwala na odczyt bez blokowania zasobu, blokuje w przypadku wystąpienia operacji zapisu. Zapewnia równocześnie aby zapis był zsynchronizowany z odczytem. Zapis danych jest natychmiast widoczny dla wątków czytających

Warto wspomnieć, że api Javy 1.8 zostały zoptymalizowane aby monitory działały z dużą ilością wątków. Przy kilku-kilkunastu wątkach monitory zapewniają wystarczającą wydajność, jeżeli mamy ponad kilkadziesiąt-kilkaset wątków warto pomyśleć o stosowaniu semaforów lub locków. W przypadku semaforów i locków zwolnienie dokonywać w bloku „finally”, mamy wtedy pewność że w przypadku błędu blokada zostanie zwolniona.

Przykłady:

Semafory

import java.util.concurrent.Semaphore;
 
/**
 * Klasa limitująca dostęp do zasobu,
 * gdzie odczyt danych może być wstrzymany aż do zwolnienia zasobu
 */
class LimitedResource{
 
   private final Semaphore semaphore;
   private int connectionCount = 0;
 
 
   public LimitedResource(int maxConnection){
 
      this.semaphore = new Semaphore(maxConnection);
   }
 
   public String getData(){
 
      try{
         semaphore.acquire();
         Thread.sleep((long) (Math.random() * 1000));
         connectionCount++;
      } catch (InterruptedException e){
         e.printStackTrace();
      } finally {
         semaphore.release();
      }
 
      return "Connection no: " + connectionCount;
   }
 
}
 
/**
 * Wątek roboczy odczytujący dane ze źródła
 */
class WorkerThread implements Runnable{
 
   LimitedResource limitedResource;
 
   public WorkerThread(LimitedResource limitedResource){
 
      this.limitedResource = limitedResource;
   }
 
   @Override
   public void run(){
 
      for (int i = 0; i < 10; i++){
         System.out.println("Worker " + Thread.currentThread().getName() 
         + " read: " + limitedResource.getData());
      }
   }
}
 
public class Thread7Semaphore{
   public static void main(String[] args){
 
      LimitedResource limitedResource = new LimitedResource(2);
 
      WorkerThread workerThread = new WorkerThread(limitedResource);
      WorkerThread workerThread2 = new WorkerThread(limitedResource);
      WorkerThread workerThread3 = new WorkerThread(limitedResource);
      WorkerThread workerThread4 = new WorkerThread(limitedResource);
 
      Thread thread = new Thread(workerThread);
      Thread thread2 = new Thread(workerThread2);
      Thread thread3 = new Thread(workerThread3);
      Thread thread4 = new Thread(workerThread4);
 
      thread.start();
      thread2.start();
      thread3.start();
      thread4.start();
   }
}

Lock

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
/**
 * Przykład synchronizacji przy pomocy obiektu Lock
 * po wypisywanych informacjach ze wąter WriteWorker
 * blokuje odczyt na czas zapisu
 */
public class Thread8Lock{
 
   private static int counter = 0;
   private static ReadWriteLock lock = new ReentrantReadWriteLock();
 
   private static Runnable workerWrite = () -> {
 
      for (int i = 0; i < 10; i++){
 
         try{
            lock.writeLock().lock();
            Thread.sleep(400);
            System.out.println("Write value...");
            counter++;
         } catch (InterruptedException e){
            e.printStackTrace();
         } finally {
            lock.writeLock().unlock();
         }
 
         try{
            Thread.sleep(100);
         } catch (InterruptedException e){
            e.printStackTrace();
         }
 
      }
   };
 
   private static Runnable workerRead = () -> {
      for (int i = 0; i < 20; i++){
         try{
            lock.readLock().lock();
            Thread.sleep(250);
            System.out.println("Read [" + i + "] value: " + counter);
 
         } catch (InterruptedException e){
            e.printStackTrace();
         } finally {
            lock.readLock().unlock();
         }
      }
   };
 
 
   public static void main(String[] args){
 
      ExecutorService executor = Executors.newFixedThreadPool(2);
      executor.submit(workerRead);
      executor.submit(workerWrite);
      executor.shutdown();
   }
 
}