Contenu du cours
Multithreading en Java
Multithreading en Java
Variables Atomiques
Nous avons déjà couvert ce qu'est l'atomicité et les problèmes qu'elle peut causer dans la première section de ce cours. À l'époque, nous avons abordé le problème en utilisant des blocs synchronisés ou des méthodes. Maintenant, nous allons explorer comment obtenir le même résultat plus facilement en utilisant une classe atomique.
Qu'est-ce que les Variables Atomiques ?
Les variables atomiques garantissent que les opérations (lecture, écriture, incrément) sur les variables sont effectuées atomiquement, c'est-à-dire qu'elles sont exécutées de manière contiguë et en toute sécurité dans un environnement multithread. Cela garantit que l'opération sera complétée entièrement, sans possibilité d'interférence d'autres threads pendant son exécution.
Pourquoi avons-nous besoin de variables atomiques ?
Sans l'utilisation de variables atomiques ou d'autres mécanismes de synchronisation, des opérations comme l'incrémentation (++)
peuvent être dangereuses. Par exemple, lorsque plusieurs threads accèdent à la même variable simultanément, les mises à jour peuvent être perdues, entraînant des résultats incorrects. Les variables atomiques résolvent ce problème en garantissant que les opérations sur elles sont exécutées séquentiellement.
Nous avons déjà discuté de ce problème lorsque l'opération d'incrémentation était décomposée en trois étapes (lecture, incrémentation, écriture), mais avec les variables atomiques, tout est fait en une seule opération !
Types de Variables Atomiques en Java
Remarque
En général, il existe de nombreuses implémentations atomiques, et nous ne les couvrirons pas toutes ici, car cela prendrait trop de temps.
Java fournit plusieurs classes de variables atomiques dans le package java.util.concurrent.atomic
, chacune conçue pour gérer un type de données spécifique :
AtomicInteger
: pour les opérations atomiques sur int ;AtomicLong
: pour les opérations atomiques sur long ;AtomicBoolean
: pour les opérations atomiques sur boolean ;AtomicReference<V>
: pour les opérations atomiques sur les objets (type générique).
Méthodes
La méthode get()
retourne la valeur actuelle d'une variable. La méthode set(V newValue)
définit une nouvelle valeur pour la variable. D'autre part, lazySet(V newValue)
est similaire à set()
, mais peut différer la mise à jour de la valeur, offrant une mise à jour ordonnée dans certaines situations.
Main
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Using `get()` to retrieve the current value String value = atomicString.get(); System.out.println("Current Value: " + value); // Using `set()` to update the value atomicString.set("New Value"); System.out.println("Value after set(): " + atomicString.get()); // Using `lazySet()` to update the value atomicString.lazySet("Lazy Set Value"); System.out.println("Value after lazySet(): " + atomicString.get()); } }
La méthode compareAndSet(V expect, V update)
met à jour la valeur si la valeur actuelle correspond à la valeur attendue. Elle retourne true
si la mise à jour a été réussie, et false
si la valeur actuelle ne correspondait pas à la valeur attendue. En revanche, la méthode getAndSet(V newValue)
définit une nouvelle valeur et retourne la valeur précédente.
Main
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { public static void main(String[] args) { // Initialize an `AtomicReference` with an initial value AtomicReference<String> atomicString = new AtomicReference<>("Initial Value"); // Demonstrate `compareAndSet` boolean success = atomicString.compareAndSet("Initial Value", "Updated Value"); System.out.println("compareAndSet success (expected true): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); success = atomicString.compareAndSet("Wrong Value", "Another Update"); System.out.println("compareAndSet success (expected false): " + success); System.out.println("Current value after compareAndSet: " + atomicString.get()); // Demonstrate `getAndSet` String previousValue = atomicString.getAndSet("New Value with getAndSet"); System.out.println("Previous value from getAndSet: " + previousValue); System.out.println("Current value after getAndSet: " + atomicString.get()); } }
Les méthodes getAndIncrement()
et getAndDecrement()
incrémentent ou décrémentent la valeur actuelle de un et retournent la valeur précédente. Ces méthodes s'appliquent aux variables atomiques numériques telles que AtomicInteger
et AtomicLong
. En revanche, les méthodes incrementAndGet()
et decrementAndGet()
incrémentent ou décrémentent également la valeur actuelle de un mais retournent la nouvelle valeur.
Main
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with initial value AtomicInteger atomicInt = new AtomicInteger(10); // Demonstrate `getAndIncrement()` for `AtomicInteger` int oldValueInt = atomicInt.getAndIncrement(); System.out.println("Value before getAndIncrement(): " + oldValueInt); // Should print 10 System.out.println("Value after getAndIncrement(): " + atomicInt.get()); // Should print 11 // Demonstrate `getAndDecrement()` for `AtomicInteger` int oldValueIntDec = atomicInt.getAndDecrement(); System.out.println("Value before getAndDecrement(): " + oldValueIntDec); // Should print 11 System.out.println("Value after getAndDecrement(): " + atomicInt.get()); // Should print 10 // Demonstrate `incrementAndGet()` for `AtomicInteger` int newValueInt = atomicInt.incrementAndGet(); System.out.println("Value after incrementAndGet(): " + newValueInt); // Should print 11 System.out.println("Current value after incrementAndGet(): " + atomicInt.get()); // Should print 11 // Demonstrate `decrementAndGet()` for `AtomicInteger` int newValueIntDec = atomicInt.decrementAndGet(); System.out.println("Value after decrementAndGet(): " + newValueIntDec); // Should print 10 System.out.println("Current value after decrementAndGet(): " + atomicInt.get()); // Should print 10 } }
La méthode getAndAdd(int delta)
ajoute la valeur spécifiée (delta) à la valeur actuelle et renvoie la valeur précédente. Cette méthode est utilisée avec des variables atomiques numériques. D'autre part, la méthode addAndGet(int delta)
ajoute également la valeur spécifiée (delta) à la valeur actuelle mais renvoie la nouvelle valeur.
Main
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { public static void main(String[] args) { // Initialize `AtomicInteger` with an initial value AtomicInteger atomicInt = new AtomicInteger(50); // Demonstrate `getAndAdd(int delta)` int previousValue = atomicInt.getAndAdd(10); System.out.println("Value before getAndAdd(10): " + previousValue); // Should print 50 System.out.println("Value after getAndAdd(10): " + atomicInt.get()); // Should print 60 // Demonstrate `getAndAdd()` with another delta int previousValue2 = atomicInt.getAndAdd(5); System.out.println("Value before getAndAdd(5): " + previousValue2); // Should print 60 System.out.println("Value after getAndAdd(5): " + atomicInt.get()); // Should print 65 // Demonstrate `addAndGet(int delta)` int newValue = atomicInt.addAndGet(20); System.out.println("Value after addAndGet(20): " + newValue); // Should print 85 System.out.println("Current value after addAndGet(20): " + atomicInt.get()); // Should print 85 // Demonstrate `addAndGet()` with another delta int newValue2 = atomicInt.addAndGet(-15); System.out.println("Value after addAndGet(-15): " + newValue2); // Should print 70 System.out.println("Current value after addAndGet(-15): " + atomicInt.get()); // Should print 70 } }
Exemples d'utilisation des variables atomiques
Exemple avec AtomicInteger
(AtomicLong
, AtomicBoolean
sont les mêmes, donc il n'y aura pas d'exemples séparés pour eux).
Tâche : Implémenter un compteur qui est sûrement incrémenté par plusieurs threads.
Main
package com.example; import java.util.concurrent.atomic.AtomicInteger; public class Main { private AtomicInteger counter = new AtomicInteger(0); public void increment() { int oldValue = counter.getAndIncrement(); System.out.println(Thread.currentThread().getName() + ": Counter was " + oldValue + ", now " + counter.get()); } public static void main(String[] args) { Main atomicCounter = new Main(); for (int i = 0; i < 5; i++) { new Thread(atomicCounter::increment).start(); } } }
Comme vous pouvez le voir, nous n'avons utilisé aucune synchronisation ici, car la variable atomique elle-même fournit cette fonction.
Cet exemple utilise AtomicInteger
pour incrémenter en toute sécurité le compteur. La méthode getAndIncrement()
renvoie d'abord la valeur actuelle de la variable, puis l'incrémente de un. Cela se produit atomiquement, garantissant que la variable est mise à jour correctement.
Exemple avec AtomicReference
Tâche : Fournir une mise à jour atomique d'une référence à un objet.
Main
package com.example; import java.util.concurrent.atomic.AtomicReference; public class Main { // `AtomicReference` to safely update and access a shared `String` value private AtomicReference<String> sharedString = new AtomicReference<>("Initial"); public void updateValue(String newValue) { // Atomically sets the new value and gets the old value String oldValue = sharedString.getAndSet(newValue); // Prints the old and new values, along with the thread name System.out.println(Thread.currentThread().getName() + ": Value was " + oldValue + ", now " + sharedString.get()); } public static void main(String[] args) { Main example = new Main(); // Creates and starts 3 threads, each updating the shared value for (int i = 0; i < 3; i++) { new Thread(() -> example.updateValue("Updated by " + Thread.currentThread().getName())).start(); } } }
AtomicReference
est utilisé ici pour mettre à jour atomiquement la valeur d'une référence de chaîne. La méthode getAndSet()
définit atomiquement la nouvelle valeur et renvoie la valeur précédente.
Remarque
Contrairement aux variables normales, qui nécessitent une synchronisation supplémentaire, les variables atomiques utilisent des primitives de bas niveau pour minimiser les frais généraux et améliorer les performances. Cela les rend particulièrement bien adaptées aux systèmes à haute concurrence.
1. Quel est l'avantage d'utiliser des variables atomiques dans la programmation multithread ?
2. Quelle méthode de variable atomique fournit un changement de valeur atomique si la valeur actuelle est la même que la valeur attendue ?
3. Que garantit la méthode set() dans les variables atomiques ?
Merci pour vos commentaires !