Guide du mot-clé synchronisé en Java

1. Vue d'ensemble

Cet article rapide sera une introduction à l'utilisation du bloc synchronisé en Java.

En termes simples, dans un environnement multithread, une condition de concurrence se produit lorsque deux ou plusieurs threads tentent de mettre à jour des données partagées mutables en même temps. Java offre un mécanisme pour éviter les conditions de concurrence en synchronisant l'accès des threads aux données partagées.

Un morceau de logique marqué avec synchronized devient un bloc synchronisé, permettant à un seul thread de s'exécuter à un moment donné .

2. Pourquoi la synchronisation?

Considérons une condition de concurrence typique où nous calculons la somme et plusieurs threads exécutent la méthode Calculate () :

public class BaeldungSynchronizedMethods { private int sum = 0; public void calculate() { setSum(getSum() + 1); } // standard setters and getters } 

Et écrivons un test simple:

@Test public void givenMultiThread_whenNonSyncMethod() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(summation::calculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, summation.getSum()); }

Nous utilisons simplement un ExecutorService avec un pool de 3 threads pour exécuter le calcul () 1000 fois.

Si nous l'exécutions en série, la sortie attendue serait de 1000, mais notre exécution multi-thread échoue presque à chaque fois avec une sortie réelle incohérente, par exemple:

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

Ce résultat n'est bien sûr pas inattendu.

Un moyen simple d'éviter la condition de concurrence est de rendre l'opération thread-safe en utilisant le mot clé synchronized .

3. Le mot-clé synchronisé

Le mot-clé synchronized peut être utilisé à différents niveaux:

  • Méthodes d'instance
  • Méthodes statiques
  • Blocs de code

Lorsque nous utilisons un bloc synchronisé , Java utilise en interne un moniteur également appelé verrouillage de moniteur ou verrouillage intrinsèque, pour assurer la synchronisation. Ces moniteurs sont liés à un objet, ainsi tous les blocs synchronisés du même objet ne peuvent avoir qu'un seul thread les exécutant en même temps.

3.1. Méthodes d'instance synchronisée

Ajoutez simplement le mot-clé synchronized dans la déclaration de méthode pour synchroniser la méthode:

public synchronized void synchronisedCalculate() { setSum(getSum() + 1); }

Notez qu'une fois que nous synchronisons la méthode, le scénario de test réussit, avec une sortie réelle de 1000:

@Test public void givenMultiThread_whenMethodSync() { ExecutorService service = Executors.newFixedThreadPool(3); SynchronizedMethods method = new SynchronizedMethods(); IntStream.range(0, 1000) .forEach(count -> service.submit(method::synchronisedCalculate)); service.awaitTermination(1000, TimeUnit.MILLISECONDS); assertEquals(1000, method.getSum()); }

Les méthodes d'instance sont synchronisées sur l'instance de la classe propriétaire de la méthode. Ce qui signifie qu'un seul thread par instance de la classe peut exécuter cette méthode.

3.2. Méthodes stati ques synchronisées

Les méthodes statiques sont synchronisées comme les méthodes d'instance:

 public static synchronized void syncStaticCalculate() { staticSum = staticSum + 1; }

Ces méthodes sont synchronisées sur l' objet Class associé à la classe et comme un seul objet Class existe par JVM et par classe, un seul thread peut s'exécuter dans une méthode statique synchronisée par classe, quel que soit le nombre d'instances dont il dispose.

Testons-le:

@Test public void givenMultiThread_whenStaticSyncMethod() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedMethods::syncStaticCalculate)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedMethods.staticSum); }

3.3. Blocs synchronisés dans les méthodes

Parfois, nous ne voulons pas synchroniser l'ensemble de la méthode mais seulement certaines instructions qu'elle contient. Ceci peut être réalisé en appliquant synchronisé à un bloc:

public void performSynchronisedTask() { synchronized (this) { setCount(getCount()+1); } }

Testons le changement:

@Test public void givenMultiThread_whenBlockSync() { ExecutorService service = Executors.newFixedThreadPool(3); BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks(); IntStream.range(0, 1000) .forEach(count -> service.submit(synchronizedBlocks::performSynchronisedTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, synchronizedBlocks.getCount()); }

Notez que nous avons passé un paramètre this au bloc synchronisé . Il s'agit de l'objet moniteur, le code à l'intérieur du bloc est synchronisé sur l'objet moniteur. En termes simples, un seul thread par objet moniteur peut s'exécuter à l'intérieur de ce bloc de code.

Dans le cas où la méthode est statique , nous passerons le nom de la classe à la place de la référence d'objet. Et la classe serait un moniteur pour la synchronisation du bloc:

public static void performStaticSyncTask(){ synchronized (SynchronisedBlocks.class) { setStaticCount(getStaticCount() + 1); } }

Testons le bloc dans la méthode statique :

@Test public void givenMultiThread_whenStaticSyncBlock() { ExecutorService service = Executors.newCachedThreadPool(); IntStream.range(0, 1000) .forEach(count -> service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask)); service.awaitTermination(100, TimeUnit.MILLISECONDS); assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount()); }

3.4. Réentrance

Le verrou derrière les méthodes et les blocs synchronisés est réentrant. Autrement dit, le thread actuel peut acquérir le même verrou synchronisé encore et encore tout en le maintenant:

Object lock = new Object(); synchronized (lock) { System.out.println("First time acquiring it"); synchronized (lock) { System.out.println("Entering again"); synchronized (lock) { System.out.println("And again"); } } }

Comme indiqué ci-dessus, alors que nous sommes dans un bloc synchronisé , nous pouvons acquérir le même verrou de moniteur à plusieurs reprises.

4. Conclusion

Dans cet article rapide, nous avons vu différentes façons d'utiliser le mot-clé synchronized pour réaliser la synchronisation des threads.

Nous avons également exploré comment une condition de concurrence peut avoir un impact sur notre application et comment la synchronisation nous aide à éviter cela. Pour plus d'informations sur la sécurité des threads à l'aide de verrous en Java, reportez-vous à notre article java.util.concurrent.Locks .

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