Guide de java.util.concurrent.Locks

1. Vue d'ensemble

En termes simples, un verrou est un mécanisme de synchronisation de thread plus flexible et plus sophistiqué que le bloc synchronisé standard .

L' interface Lock existe depuis Java 1.5. Il est défini dans le package java.util.concurrent.lock et fournit des opérations étendues pour le verrouillage.

Dans cet article, nous explorerons différentes implémentations de l' interface Lock et leurs applications.

2. Différences entre verrouillage et bloc synchronisé

Il y a peu de différences entre l'utilisation du bloc synchronisé et l'utilisation des API de verrouillage :

  • Un bloc synchronisé est entièrement contenu dans une méthode - nous pouvons avoir les opérations lock () et unlock () de l' API Lock dans des méthodes séparées
  • Un bloc synchronisé ne prend pas en charge l'équité, tout thread peut acquérir le verrou une fois libéré, aucune préférence ne peut être spécifiée. Nous pouvons atteindre l'équité dans les API de verrouillage en spécifiant la propriété fairness . Il s'assure que le fil d'attente le plus long a accès au verrou
  • Un thread est bloqué s'il ne peut pas accéder au bloc synchronisé . L' API Lock fournit la méthode tryLock () . Le thread acquiert le verrou uniquement s'il est disponible et n'est détenu par aucun autre thread. Cela réduit le temps de blocage du thread en attente du verrou
  • Un thread qui est en "attente" pour acquérir l'accès au bloc synchronisé ne peut pas être interrompu. L' API Lock fournit une méthode lockInterruptably () qui peut être utilisée pour interrompre le thread lorsqu'il attend le verrou

3. Verrouiller l' API

Jetons un coup d'œil aux méthodes de l' interface Lock :

  • void lock () - acquiert le verrou s'il est disponible; si le verrou n'est pas disponible, un thread est bloqué jusqu'à ce que le verrou soit libéré
  • void lockInterruptably () - c'est similaire à lock (), mais cela permet au thread bloqué d'être interrompu et de reprendre l'exécution via une exception java.lang.InterruptedException lancée.
  • boolean tryLock () - c'est une version non bloquante de laméthode lock () ; il tente d'acquérir le verrou immédiatement, retourne true si le verrouillage réussit
  • boolean tryLock (long timeout, TimeUnit timeUnit) - c'est similaire à tryLock (), sauf qu'il attend le délai imparti avant de renoncer à essayer d'acquérir le verrou
  • void unlock () - déverrouille l' instance de verrouillage

Une instance verrouillée doit toujours être déverrouillée pour éviter une situation de blocage. Un bloc de code recommandé pour utiliser le verrou doit contenir un bloc try / catch et enfin :

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

En plus de l' interface Lock , nous avons une interface ReadWriteLock qui maintient une paire de verrous, un pour les opérations en lecture seule et un pour l'opération d'écriture. Le verrou de lecture peut être maintenu simultanément par plusieurs threads tant qu'il n'y a pas d'écriture.

ReadWriteLock déclare des méthodes pour acquérir des verrous de lecture ou d'écriture:

  • Lock readLock () - renvoie le verrou utilisé pour la lecture
  • Lock writeLock () - retourne le verrou utilisé pour l'écriture

4. Verrouiller les implémentations

4.1. ReentrantLock

La classe ReentrantLock implémente l' interface Lock . Il offre la même simultanéité et la même sémantique de mémoire que le verrou implicite du moniteur accessible à l'aide de méthodes et d'instructions synchronisées , avec des capacités étendues.

Voyons comment nous pouvons utiliser ReenrtantLock pour la synchronisation:

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

Nous devons nous assurer que nous encapsulons les appels lock () et unlock () dans le bloc try-finally pour éviter les situations de blocage.

Voyons comment fonctionne tryLock () :

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

Dans ce cas, le thread appelant tryLock () attendra une seconde et abandonnera l'attente si le verrou n'est pas disponible.

4.2. RéentrantReadWriteLock

La classe ReentrantReadWriteLock implémente l' interface ReadWriteLock .

Voyons les règles pour acquérir le ReadLock ou le WriteLock par un thread:

  • Verrou de lecture - si aucun thread n'a acquis le verrou d'écriture ou demandé pour celui-ci, plusieurs threads peuvent acquérir le verrou de lecture
  • Verrou d'écriture - si aucun thread ne lit ou n'écrit, un seul thread peut acquérir le verrou d'écriture

Voyons comment utiliser le 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(); } } //... }

Pour les deux méthodes d'écriture, nous devons entourer la section critique avec le verrou d'écriture, un seul thread peut y accéder:

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(); } }

Pour les deux méthodes de lecture, nous devons entourer la section critique du verrou de lecture. Plusieurs threads peuvent accéder à cette section si aucune opération d'écriture n'est en cours.

4.3. StampedLock

StampedLock est introduit dans Java 8. Il prend également en charge les verrous en lecture et en écriture. Cependant, les méthodes d'acquisition de verrou renvoient un tampon utilisé pour libérer un verrou ou pour vérifier si le verrou est toujours valide:

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

Une autre fonctionnalité fournie par StampedLock est le verrouillage optimiste. La plupart du temps, les opérations de lecture n'ont pas besoin d'attendre la fin de l'opération d'écriture et, par conséquent, le verrouillage de lecture complet n'est pas nécessaire.

Au lieu de cela, nous pouvons mettre à niveau pour verrouiller en lecture:

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

Dans cet article, nous avons vu différentes implémentations de l' interface Lock et de la classe StampedLock nouvellement introduite . Nous avons également exploré comment nous pouvons utiliser la classe Condition pour travailler avec plusieurs conditions.

Le code complet de ce didacticiel est disponible à l'adresse over sur GitHub.