Utilisation d'un objet Mutex en Java

1. Vue d'ensemble

Dans ce tutoriel, nous verrons différentes manières d'implémenter un mutex en Java .

2. Mutex

Dans une application multithread, deux ou plusieurs threads peuvent avoir besoin d'accéder à une ressource partagée en même temps, ce qui entraîne un comportement inattendu. Des exemples de telles ressources partagées sont les structures de données, les périphériques d'entrée-sortie, les fichiers et les connexions réseau.

Nous appelons ce scénario une condition de concurrence . Et, la partie du programme qui accède à la ressource partagée est connue comme la section critique . Donc, pour éviter une condition de concurrence, nous devons synchroniser l'accès à la section critique.

Un mutex (ou exclusion mutuelle) est le type le plus simple de synchroniseur - il garantit qu'un seul thread peut exécuter la section critique d'un programme informatique à la fois .

Pour accéder à une section critique, un thread acquiert le mutex, puis accède à la section critique, et enfin libère le mutex. En attendant, tous les autres threads bloquent jusqu'à la sortie du mutex. Dès qu'un thread quitte la section critique, un autre thread peut entrer dans la section critique.

3. Pourquoi Mutex?

Tout d'abord, prenons un exemple de classe SequenceGeneraror , qui génère la séquence suivante en incrémentant la valeur currentValue de un à chaque fois:

public class SequenceGenerator { private int currentValue = 0; public int getNextSequence() { currentValue = currentValue + 1; return currentValue; } }

Maintenant, créons un cas de test pour voir comment cette méthode se comporte lorsque plusieurs threads tentent d'y accéder simultanément:

@Test public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception { int count = 1000; Set uniqueSequences = getUniqueSequences(new SequenceGenerator(), count); Assert.assertEquals(count, uniqueSequences.size()); } private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(3); Set uniqueSequences = new LinkedHashSet(); List
    
      futures = new ArrayList(); for (int i = 0; i < count; i++) { futures.add(executor.submit(generator::getNextSequence)); } for (Future future : futures) { uniqueSequences.add(future.get()); } executor.awaitTermination(1, TimeUnit.SECONDS); executor.shutdown(); return uniqueSequences; }
    

Une fois que nous exécutons ce cas de test, nous pouvons voir qu'il échoue la plupart du temps avec une raison similaire à:

java.lang.AssertionError: expected: but was: at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645)

L' uniqueSequences est censé avoir la taille égale au nombre de fois que nous avons exécuté la méthode getNextSequence dans notre cas de test. Cependant, ce n'est pas le cas en raison de la condition de concurrence. De toute évidence, nous ne voulons pas de ce comportement.

Donc, pour éviter de telles conditions de concurrence , nous devons nous assurer qu'un seul thread peut exécuter la méthode getNextSequence à la fois . Dans de tels scénarios, nous pouvons utiliser un mutex pour synchroniser les threads.

Il existe différentes manières, nous pouvons implémenter un mutex en Java. Ensuite, nous verrons les différentes façons d'implémenter un mutex pour notre classe SequenceGenerator .

4. Utilisation d' un mot-clé synchronisé

Tout d'abord, nous aborderons le mot clé synchronized , qui est le moyen le plus simple d'implémenter un mutex en Java.

Chaque objet en Java est associé à un verrou intrinsèque. La méthode synchronisée et le bloc synchronisé utilisent ce verrou intrinsèque pour limiter l'accès de la section critique à un seul thread à la fois.

Par conséquent, lorsqu'un thread appelle une méthode synchronisée ou entre dans un bloc synchronisé , il acquiert automatiquement le verrou. Le verrou se libère lorsque la méthode ou le bloc se termine ou qu'une exception est levée.

Changement Let getNextSequence d'avoir un mutex, en ajoutant simplement le synchronisé mot - clé:

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator { @Override public synchronized int getNextSequence() { return super.getNextSequence(); } }

Le bloc synchronisé est similaire à la méthode synchronisée , avec plus de contrôle sur la section critique et l'objet que nous pouvons utiliser pour le verrouillage.

Alors, voyons maintenant comment nous pouvons utiliser la synchronisation bloc pour synchroniser sur un objet mutex personnalisé :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator { private Object mutex = new Object(); @Override public int getNextSequence() { synchronized (mutex) { return super.getNextSequence(); } } }

5. Utilisation de ReentrantLock

La classe ReentrantLock a été introduite dans Java 1.5. Elle offre plus de flexibilité et de contrôle que l' approche par mots clés synchronisés .

Voyons comment nous pouvons utiliser ReentrantLock pour parvenir à une exclusion mutuelle:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator { private ReentrantLock mutex = new ReentrantLock(); @Override public int getNextSequence() { try { mutex.lock(); return super.getNextSequence(); } finally { mutex.unlock(); } } }

6. Utilisation du sémaphore

Comme ReentrantLock , la classe Semaphore a également été introduite dans Java 1.5.

Alors que dans le cas d'un mutex, un seul thread peut accéder à une section critique, Semaphore permet à un nombre fixe de threads d'accéder à une section critique . Par conséquent, nous pouvons également implémenter un mutex en définissant le nombre de threads autorisés dans un sémaphore sur un .

Créons maintenant une autre version thread-safe de SequenceGenerator en utilisant Semaphore :

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator { private Semaphore mutex = new Semaphore(1); @Override public int getNextSequence() { try { mutex.acquire(); return super.getNextSequence(); } catch (InterruptedException e) { // exception handling code } finally { mutex.release(); } } }

7. Utilisation de la classe Monitor de Guava

Jusqu'à présent, nous avons vu les options pour implémenter mutex en utilisant les fonctionnalités fournies par Java.

Cependant, la classe Monitor de la bibliothèque Guava de Google est une meilleure alternative à la classe ReentrantLock . Selon sa documentation, le code utilisant Monitor est plus lisible et moins sujet aux erreurs que le code utilisant ReentrantLock .

Tout d'abord, nous allons ajouter la dépendance Maven pour Guava:

 com.google.guava guava 28.0-jre 

Maintenant, nous allons écrire une autre sous-classe de SequenceGenerator en utilisant la classe Monitor :

public class SequenceGeneratorUsingMonitor extends SequenceGenerator { private Monitor mutex = new Monitor(); @Override public int getNextSequence() { mutex.enter(); try { return super.getNextSequence(); } finally { mutex.leave(); } } }

8. Conclusion

Dans ce didacticiel, nous avons examiné le concept de mutex. Nous avons également vu les différentes façons de l'implémenter en Java.

Comme toujours, le code source complet des exemples de code utilisés dans ce didacticiel est disponible à l'adresse over sur GitHub.