Guida a java.util.concurrent.Locks

1. Panoramica

In poche parole, un blocco è un meccanismo di sincronizzazione dei thread più flessibile e sofisticato rispetto al blocco sincronizzato standard .

L' interfaccia Lock esiste da Java 1.5. È definito all'interno del pacchetto java.util.concurrent.lock e fornisce operazioni estese per il blocco.

In questo articolo, esploreremo diverse implementazioni dell'interfaccia Lock e delle loro applicazioni.

2. Differenze tra blocco e blocco sincronizzato

Ci sono poche differenze tra l'uso del blocco sincronizzato e l'utilizzo delle API di blocco :

  • Un blocco sincronizzato è completamente contenuto in un metodo: possiamo avere le operazioni lock () e unlock () dell'API Lock in metodi separati
  • Un blocco sincronizzato non supporta l'equità, qualsiasi thread può acquisire il blocco una volta rilasciato, non è possibile specificare alcuna preferenza. Possiamo ottenere l'equità all'interno delle API di blocco specificando la proprietà di equità . Si assicura che il thread in attesa più lungo abbia accesso al blocco
  • Un thread viene bloccato se non riesce a ottenere l'accesso al blocco sincronizzato . L' API Lock fornisce il metodo tryLock () . Il thread acquisisce il blocco solo se è disponibile e non è tenuto da nessun altro thread. Ciò riduce il tempo di blocco del thread in attesa del blocco
  • Un thread che è in stato di “attesa” per acquisire l'accesso al blocco sincronizzato , non può essere interrotto. L' API Lock fornisce un metodo lockInterruptibly () che può essere utilizzato per interrompere il thread quando è in attesa del blocco

3. Blocca API

Diamo un'occhiata ai metodi nell'interfaccia di blocco :

  • void lock () - acquisisce il lock se è disponibile; se il blocco non è disponibile, un thread viene bloccato fino a quando il blocco non viene rilasciato
  • void lockInterruptibly () - è simile a lock (), ma consente di interrompere il thread bloccato e riprendere l'esecuzione tramite un'eccezione java.lang.InterructedException generata
  • boolean tryLock () - questa è una versione non bloccante delmetodo lock () ; tenta di acquisire immediatamente il blocco, restituisce true se il blocco riesce
  • booleano tryLock (long timeout, TimeUnit timeUnit) - questo è simile a tryLock (), eccetto che attende il timeout specificato prima di rinunciare a provare ad acquisire il Lock
  • void unlock () - sblocca l' istanza Lock

Un'istanza bloccata dovrebbe essere sempre sbloccata per evitare una condizione di deadlock. Un blocco di codice consigliato per utilizzare il blocco dovrebbe contenere un try / catch e infine un blocco:

Lock lock = ...; lock.lock(); try { // access to the shared resource } finally { lock.unlock(); }

In aggiunta al blocco di interfaccia , abbiamo un ReadWriteLock interfaccia che mantiene una coppia di blocchi, uno per le operazioni di sola lettura, e una per l'operazione di scrittura. Il blocco di lettura può essere mantenuto simultaneamente da più thread finché non c'è scrittura.

ReadWriteLock dichiara i metodi per acquisire i blocchi di lettura o scrittura:

  • Lock readLock () : restituisce il blocco utilizzato per la lettura
  • Lock writeLock () : restituisce il blocco utilizzato per la scrittura

4. Bloccare le implementazioni

4.1. ReentrantLock

La classe ReentrantLock implementa l' interfaccia Lock . Offre la stessa concorrenza e semantica della memoria, come il blocco del monitor implicito a cui si accede utilizzando metodi e istruzioni sincronizzati , con funzionalità estese.

Vediamo, come possiamo usare ReenrtantLock per la sincronizzazione:

public class SharedObject { //... ReentrantLock lock = new ReentrantLock(); int counter = 0; public void perform() { lock.lock(); try { // Critical section here count++; } finally { lock.unlock(); } } //... }

Dobbiamo assicurarci di racchiudere le chiamate lock () e unlock () nel blocco try- latest per evitare situazioni di deadlock.

Vediamo come funziona tryLock () :

public void performTryLock(){ //... boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS); if(isLockAcquired) { try { //Critical section here } finally { lock.unlock(); } } //... } 

In questo caso, il thread che chiama tryLock (), aspetterà un secondo e rinuncerà ad aspettare se il blocco non è disponibile.

4.2. ReentrantReadWriteLock

La classe ReentrantReadWriteLock implementa l' interfaccia ReadWriteLock .

Vediamo le regole per acquisire ReadLock o WriteLock da un thread:

  • Blocco di lettura : se nessun thread ha acquisito il blocco di scrittura o richiesto per esso, più thread possono acquisire il blocco di lettura
  • Blocco scrittura : se nessun thread sta leggendo o scrivendo, solo un thread può acquisire il blocco di scrittura

Vediamo come utilizzare ReadWriteLock :

public class SynchronizedHashMapWithReadWriteLock { Map syncHashMap = new HashMap(); ReadWriteLock lock = new ReentrantReadWriteLock(); // ... Lock writeLock = lock.writeLock(); public void put(String key, String value) { try { writeLock.lock(); syncHashMap.put(key, value); } finally { writeLock.unlock(); } } ... public String remove(String key){ try { writeLock.lock(); return syncHashMap.remove(key); } finally { writeLock.unlock(); } } //... }

Per entrambi i metodi di scrittura, dobbiamo circondare la sezione critica con il blocco di scrittura, solo un thread può accedervi:

Lock readLock = lock.readLock(); //... public String get(String key){ try { readLock.lock(); return syncHashMap.get(key); } finally { readLock.unlock(); } } public boolean containsKey(String key) { try { readLock.lock(); return syncHashMap.containsKey(key); } finally { readLock.unlock(); } }

Per entrambi i metodi di lettura, dobbiamo racchiudere la sezione critica con il blocco di lettura. Più thread possono accedere a questa sezione se non è in corso alcuna operazione di scrittura.

4.3. StampedLock

StampedLock è stato introdotto in Java 8. Supporta anche i blocchi di lettura e scrittura. Tuttavia, i metodi di acquisizione del blocco restituiscono un timbro che viene utilizzato per rilasciare un blocco o per verificare se il blocco è ancora valido:

public class StampedLockDemo { Map map = new HashMap(); private StampedLock lock = new StampedLock(); public void put(String key, String value){ long stamp = lock.writeLock(); try { map.put(key, value); } finally { lock.unlockWrite(stamp); } } public String get(String key) throws InterruptedException { long stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlockRead(stamp); } } }

Un'altra caratteristica fornita da StampedLock è il blocco ottimistico. La maggior parte delle volte le operazioni di lettura non devono attendere il completamento dell'operazione di scrittura e, di conseguenza, il blocco di lettura completo non è necessario.

Invece, possiamo eseguire l'aggiornamento per leggere il blocco:

public String readWithOptimisticLock(String key) { long stamp = lock.tryOptimisticRead(); String value = map.get(key); if(!lock.validate(stamp)) { stamp = lock.readLock(); try { return map.get(key); } finally { lock.unlock(stamp); } } return value; }

5. Working With Conditions

The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.

This can occur when a thread acquires the access to the critical section but doesn't have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue, which still doesn't have any data to consume.

Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication. Conditions have similar mechanisms, but in addition, we can specify multiple conditions:

public class ReentrantLockWithCondition { Stack stack = new Stack(); int CAPACITY = 5; ReentrantLock lock = new ReentrantLock(); Condition stackEmptyCondition = lock.newCondition(); Condition stackFullCondition = lock.newCondition(); public void pushToStack(String item){ try { lock.lock(); while(stack.size() == CAPACITY) { stackFullCondition.await(); } stack.push(item); stackEmptyCondition.signalAll(); } finally { lock.unlock(); } } public String popFromStack() { try { lock.lock(); while(stack.size() == 0) { stackEmptyCondition.await(); } return stack.pop(); } finally { stackFullCondition.signalAll(); lock.unlock(); } } }

6. Conclusion

In questo articolo, abbiamo visto diverse implementazioni dell'interfaccia Lock e della nuova classe StampedLock . Abbiamo anche esplorato come possiamo utilizzare la classe Condizione per lavorare con più condizioni.

Il codice completo per questo tutorial è disponibile su GitHub.