12.3Synchronisation et concurrence
12.3.1 – Problème de race condition
Une race condition (condition de course) se produit quand plusieurs threads accèdent et modifient simultanément la même variable, ce qui peut conduire à des résultats imprévisibles ou incorrects.
⚠️ Exemple de problème
Exemple : Compteur partagé sans synchronisation
public class Compteur {
private int valeur = 0;
public void incrementer() {
valeur++; // Opération non atomique !
}
public int getValeur() {
return valeur;
}
}
// Utilisation avec plusieurs threads
public class TestRaceCondition {
public static void main(String[] args) throws InterruptedException {
Compteur compteur = new Compteur();
// Créer 1000 threads qui incrémentent chacun
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
compteur.incrementer();
});
threads[i].start();
}
// Attendre que tous les threads se terminent
for (Thread t : threads) {
t.join();
}
// Résultat attendu : 1000
// Résultat réel : souvent moins (par exemple 987, 992, etc.)
System.out.println("Valeur finale : " + compteur.getValeur());
}
}
L'opération valeur++ n'est pas atomique. Elle se décompose en plusieurs étapes :
- Lire la valeur actuelle
- L'incrémenter
- Écrire la nouvelle valeur
Si deux threads font ces opérations en même temps, ils peuvent tous les deux lire la même valeur, l'incrémenter, et écrire le même résultat, ce qui fait perdre une incrémentation.
🔍 Scénario de race condition
- Thread 1 lit valeur = 5
- Thread 2 lit valeur = 5 (en mĂŞme temps)
- Thread 1 calcule 5 + 1 = 6
- Thread 2 calcule 5 + 1 = 6
- Thread 1 écrit 6
- Thread 2 écrit 6 (écrase la valeur de Thread 1)
- Résultat : valeur = 6 au lieu de 7
12.3.2 – Synchronisation avec synchronized
Le mot-clé synchronized permet de synchroniser l'accès aux méthodes ou blocs de code, garantissant qu'un seul thread à la fois peut exécuter le code synchronisé.
🔒 Méthode synchronisée
Exemple : Méthode synchronisée
public class CompteurSynchronise {
private int valeur = 0;
// Méthode synchronisée : un seul thread à la fois peut l'exécuter
public synchronized void incrementer() {
valeur++;
}
public synchronized int getValeur() {
return valeur;
}
}
- Quand un thread entre dans une méthode
synchronized, il acquiert un verrou (lock) sur l'objet - Les autres threads doivent attendre que le verrou soit libéré
- Une fois la méthode terminée, le verrou est libéré et le thread suivant peut entrer
- Cela garantit que les opérations sont atomiques
🔒 Bloc synchronisé
Vous pouvez aussi synchroniser seulement une partie du code avec un bloc synchronized :
public class Compteur {
private int valeur = 0;
private Object verrou = new Object(); // Objet utilisé comme verrou
public void incrementer() {
// Code non synchronisé (peut être exécuté en parallèle)
System.out.println("Avant incrémentation");
// Bloc synchronisé (un seul thread à la fois)
synchronized (verrou) {
valeur++;
}
// Code non synchronisé
System.out.println("Après incrémentation");
}
}
💻 Exemple complet : Compteur synchronisé
public class CompteurSynchronise {
private int valeur = 0;
public synchronized void incrementer() {
valeur++;
}
public synchronized void decrementer() {
valeur--;
}
public synchronized int getValeur() {
return valeur;
}
}
// Test
public class TestSynchronisation {
public static void main(String[] args) throws InterruptedException {
CompteurSynchronise compteur = new CompteurSynchronise();
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
compteur.incrementer();
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
// Maintenant le résultat est toujours correct : 1000
System.out.println("Valeur finale : " + compteur.getValeur());
}
}
📊 Synchronisation sur this
Quand vous synchronisez une méthode, c'est équivalent à synchroniser sur this :
// Ces deux méthodes sont équivalentes :
// Méthode 1 : Méthode synchronisée
public synchronized void methode() {
// code
}
// Méthode 2 : Bloc synchronisé sur this
public void methode() {
synchronized (this) {
// code
}
}
12.3.3 – Collections thread-safe
Les collections standard de Java (ArrayList, HashMap, etc.) ne sont pas thread-safe. Pour utiliser des collections dans un environnement multi-thread, vous devez utiliser des versions thread-safe ou synchroniser l'accès.
⚠️ Problème avec les collections non thread-safe
import java.util.ArrayList;
import java.util.List;
List<String> liste = new ArrayList<>(); // Non thread-safe
// Plusieurs threads ajoutent des éléments
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
liste.add("Element " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
liste.add("Element " + i);
}
});
t1.start();
t2.start();
// Problème : peut causer des erreurs ou des données perdues
âś… Solutions
Solution 1 : Collections.synchronizedList()
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
List<String> liste = Collections.synchronizedList(new ArrayList<>());
// Maintenant thread-safe
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
liste.add("Element " + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
liste.add("Element " + i);
}
});
t1.start();
t2.start();
Solution 2 : Collections thread-safe (java.util.concurrent)
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentHashMap;
// Liste thread-safe
List<String> liste = new CopyOnWriteArrayList<>();
// Map thread-safe
Map<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList: Liste thread-safeConcurrentHashMap: Map thread-safeBlockingQueue: File thread-safeConcurrentLinkedQueue: Queue thread-safe
12.3.4 – Deadlock (interblocage)
Un deadlock (interblocage) se produit quand deux ou plusieurs threads s'attendent mutuellement pour libérer des verrous, créant une situation de blocage permanent.
⚠️ Exemple de deadlock
public class DeadlockExemple {
private final Object verrou1 = new Object();
private final Object verrou2 = new Object();
public void methode1() {
synchronized (verrou1) {
System.out.println("Thread 1 : Verrou 1 acquis");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
synchronized (verrou2) { // Attend verrou2
System.out.println("Thread 1 : Verrou 2 acquis");
}
}
}
public void methode2() {
synchronized (verrou2) {
System.out.println("Thread 2 : Verrou 2 acquis");
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
synchronized (verrou1) { // Attend verrou1
System.out.println("Thread 2 : Verrou 1 acquis");
}
}
}
}
// Utilisation
DeadlockExemple exemple = new DeadlockExemple();
Thread t1 = new Thread(() -> exemple.methode1());
Thread t2 = new Thread(() -> exemple.methode2());
t1.start();
t2.start();
// Deadlock : les deux threads sont bloqués indéfiniment
- Thread 1 acquiert verrou1, attend verrou2
- Thread 2 acquiert verrou2, attend verrou1
- Chaque thread attend que l'autre libère son verrou
- Blocage permanent !
✅ Prévention des deadlocks
- Ordre des verrous : Toujours acquérir les verrous dans le même ordre
- Timeout : Utiliser des verrous avec timeout
- Éviter les verrous multiples : Minimiser le nombre de verrous nécessaires
- Détection : Surveiller les threads bloqués
💡 Points clés à retenir
- Race condition : Problème quand plusieurs threads modifient la même variable
- synchronized : Garantit qu'un seul thread exécute le code à la fois
- Méthode synchronisée :
public synchronized void methode() - Bloc synchronisé :
synchronized (objet) { ... } - Collections thread-safe : Utiliser CopyOnWriteArrayList, ConcurrentHashMap, etc.
- Deadlock : Blocage mutuel entre threads
- Prévention : Acquérir les verrous dans le même ordre
java.util.concurrent plutĂ´t que de synchroniser manuellement les collections standard.