Guide sur java.util.concurrent.Locks

Guide de java.util.concurrent.Locks

 

1. Vue d'ensemble

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

L'interfaceLock existe depuis Java 1.5. Il est défini dans le packagejava.util.concurrent.lock et fournit de nombreuses opérations de verrouillage.

Dans cet article, nous allons explorer différentes implémentations de l'interfaceLock et de leurs applications.

2. Différences entre verrouillage et bloc synchronisé

Il existe peu de différences entre l’utilisation deblock synchronisés et l’utilisation des APILock:

  • A synchronized block is fully contained within a method – nous pouvons avoir les opérationslock() etunlock() des APILock dans des méthodes séparées

  • Un synchronized block ne prend pas en charge l'équité, n'importe quel thread peut acquérir le verrou une fois libéré, aucune préférence ne peut être spécifiée. We can achieve fairness within the Lock APIs by specifying the fairness property. Il s'assure que le fil d'attente le plus long en attente a accès au verrou

  • Un thread est bloqué s’il ne peut pas accéder auxblock synchronisés. The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. Cela réduit le temps de blocage du thread en attente du verrou

  • Un thread qui est en «attente» d’acquérir l’accès àsynchronized block ne peut pas être interrompu. The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock

3. APILock

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

  • *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 lockInterruptibly() - ceci est similaire aulock(), mais cela permet au thread bloqué d'être interrompu et de reprendre l'exécution via unjava.lang.InterruptedException lancé

  • boolean tryLock() - il s'agit d'une version non bloquante de la méthodelock(); il tente d'acquérir le verrou immédiatement, retourne true si le verrouillage réussit

  • *boolean tryLock(long timeout, TimeUnit timeUnit)* – ceci est similaire àtryLock(), sauf qu'il attend le délai imparti avant de renoncer à essayer d'acquérir lesLock

  • void unlock() - déverrouille l'instanceLock

Une instance verrouillée doit toujours être déverrouillée pour éviter la condition de blocage. Un bloc de code recommandé pour utiliser le verrou doit contenir un bloctry/catch etfinally:

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

En plus de l'interfaceLock,, nous avons une interfaceReadWriteLock 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() - renvoie le verrou utilisé pour l'écriture

4. Verrouiller les implémentations

4.1. ReentrantLock

La classeReentrantLock implémente l'interfaceLock. 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 des méthodes et des instructionssynchronized, avec des capacités étendues.

Voyons comment nous pouvons utiliser la synchronisation deReenrtantLock for:

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 appelslock () etunlock() dans le bloctry-finally pour éviter les situations de blocage.

Voyons comment fonctionne letryLock():

public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}

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

4.2. ReentrantReadWriteLock

La classeReentrantReadWriteLock implémente l'interfaceReadWriteLock.

Voyons les règles d’acquisition desReadLock ouWriteLock par un thread:

  • Read Lock - si aucun thread n'a acquis le verrou d'écriture ou demandé pour celui-ci, plusieurs threads peuvent acquérir le verrou de lecture

  • Write Lock - si aucun thread ne lit ou n'écrit, un seul thread peut acquérir le verrou d'écriture

Voyons comment utiliser lesReadWriteLock:

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 en é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. Toutefois, 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 parStampedLock 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 lire le verrou:

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. Travailler avecConditions

La classeCondition permet à un thread d'attendre qu'une condition se produise lors de l'exécution de la section critique.

Cela peut se produire lorsqu'un thread acquiert l'accès à la section critique mais n'a pas la condition nécessaire pour effectuer son opération. Par exemple, un thread de lecture peut accéder au verrou d’une file d’attente partagée, qui n’a toujours pas de données à consommer.

Traditionnellement, Java fournit les méthodeswait(), notify() and notifyAll() pour l'intercommunication des threads. Conditions ont des mécanismes similaires, mais en plus, nous pouvons spécifier plusieurs 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'interfaceLock et de la classeStampedLock nouvellement introduite. Nous avons également exploré comment nous pouvons utiliser la classeCondition pour travailler avec plusieurs conditions.

Le code complet de ce didacticiel est disponibleover on GitHub.