CodeGym /Cours /JAVA 25 SELF /ReentrantLock et ReadWriteLock : différences, exempl...

ReentrantLock et ReadWriteLock : différences, exemples

JAVA 25 SELF
Niveau 52 , Leçon 2
Disponible

1. ReentrantLock : un verrou flexible

Le mot-clé synchronized convient très bien aux cas de base : on peut protéger rapidement et simplement une méthode ou un bloc de code. Mais parfois on veut davantage :

  • Contrôler explicitement le verrou (par exemple, tenter de l’acquérir et, en cas d’échec, ne pas attendre).
  • Séparer les droits « lecture » et « écriture » sur une ressource.
  • Interrompre l’attente d’un verrou.
  • Diagnostiquer qui a acquis ou libéré le verrou et quand.

C’est pour ces besoins que les classes ReentrantLock et ReadWriteLock ont été conçues. Elles offrent plus de contrôle et de possibilités que le bon vieux synchronized.

De quoi s’agit-il ?

ReentrantLock est une classe qui implémente l’interface Lock. Elle fonctionne à peu près comme synchronized, mais avec des fonctionnalités supplémentaires. La différence principale : la gestion du verrou devient explicite — vous appelez vous‑même lock() et unlock().

Point intéressant : le mot reentrant signifie qu’un thread peut acquérir le même verrou plusieurs fois de suite sans provoquer d’interblocage. C’est utile si une méthode s’appelle récursivement ou si l’on travaille avec un verrou partagé dans une chaîne d’appels.

Syntaxe d’utilisation

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // On acquiert le verrou
        try {
            value++;
        } finally {
            lock.unlock(); // Toujours libérer le verrou !
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

À noter :
Les appels à lock() et unlock() doivent toujours être associés à un bloc try...finally. Si vous oubliez d’appeler unlock(), aucun autre thread ne pourra entrer dans la section protégée — vous obtiendrez un blocage permanent.

Fonctionnalités de ReentrantLock

Tentative d’acquisition :
On peut essayer d’acquérir le verrou sans attendre indéfiniment :

if (lock.tryLock()) {
    try {
        // Traitement
    } finally {
        lock.unlock();
    }
} else {
    // Échec d’acquisition — faire autre chose
}

Attente avec délai (timeout) :

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // Acquis en 100 ms
}

Vérifier si le verrou est détenu :

if (lock.isLocked()) { ... }

Diagnostic de la file d’attente, « équité » du verrou et autres petits plus.

3. Exemple : incrément d’un compteur avec ReentrantLock

Faisons évoluer notre application console (par exemple, en simulant le traitement de commandes depuis différents threads). Comparons le travail avec synchronized et avec ReentrantLock.

Exemple avec synchronized

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Exemple équivalent avec ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

Quels avantages ?

  • On peut tenter d’acquérir le verrou sans attendre indéfiniment (tryLock()).
  • On peut implémenter une logique complexe : par exemple, acquérir plusieurs verrous dans un ordre précis (utile pour des structures de données complexes).
  • On peut « déverrouiller » depuis un autre endroit (mais à manier avec beaucoup de prudence — toujours penser à unlock() !).

4. ReadWriteLock : verrou pour lecture et écriture

Qu’est-ce que c’est ?

ReadWriteLock n’est pas juste un verrou, mais un répartiteur d’accès intelligent. Son implémentation principale est ReentrantReadWriteLock, qui sépare les verrous en deux catégories : lecture et écriture.

Lorsque les threads ne font que lire et que personne ne modifie les données, ils peuvent travailler en parallèle — la lecture ne gêne pas la lecture. Mais dès que quelqu’un veut modifier, tous les autres doivent attendre : l’écriture n’autorise qu’un seul participant et requiert le silence.

Cette approche est particulièrement utile là où il y a beaucoup de lectures et peu de modifications — par exemple, dans un catalogue de produits que les utilisateurs consultent en permanence mais ne mettent à jour qu’occasionnellement.

Syntaxe d’utilisation

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Exemple d’utilisation dans notre application

Supposons que nous ayons une base de commandes que tous les threads lisent (par exemple, pour rechercher une commande), mais que de nouvelles commandes arrivent de temps en temps (opération d’écriture).

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // Ajout d’une commande (nécessite writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Récupérer une copie de toutes les commandes (lecture en parallèle possible)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // Retourner une copie pour éviter les courses
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Que se passe-t-il ?

  • Tant que personne n’écrit, des milliers de threads peuvent lire la liste des commandes simultanément.
  • Dès qu’un thread commence à ajouter une commande, les lectures sont bloquées afin d’éviter des données incohérentes.

5. Comparaison : quand utiliser quoi ?

Scénario synchronized ReentrantLock ReadWriteLock
Synchronisation simple ✖ (superflu)
Besoin d’un délai d’attente/tryLock
Beaucoup de lectures, peu d’écritures ✔ (gain significatif)
Diagnostic/statistiques nécessaires
Verrouillage récursif ✔ (réentrance)

Conclusion :

  • Pour les cas simples — utilisez synchronized.
  • Pour plus de flexibilité — ReentrantLock.
  • Pour les scénarios « lecture fréquente, écriture rare » — ReadWriteLock.

6. Visualisation : schéma de fonctionnement de ReadWriteLock

flowchart LR
    subgraph Lecture
      T1[Thread 1] -- Lecture --> Orders
      T2[Thread 2] -- Lecture --> Orders
      T3[Thread 3] -- Lecture --> Orders
    end
    subgraph Écriture
      T4[Thread 4] -- Écriture (addOrder) --> Orders
    end
    Orders[Liste des commandes]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

Tant qu’aucun thread n’écrit, tous peuvent lire en parallèle. Dès qu’une écriture apparaît, les autres threads attendent la libération de writeLock.

7. Particularités d’implémentation et nuances

« Équité » (fairness)

Dans ReentrantLock et ReentrantReadWriteLock, on peut activer un mode « équitable » (fair mode) : les threads sont servis dans l’ordre de la file, et non pas au plus rapide. Cela évite l’« affamement » de certains threads, mais peut réduire les performances.

Lock fairLock = new ReentrantLock(true); // true — mode équitable
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

Pièges potentiels

  • unlock oublié : si unlock() n’est pas appelé, vous obtenez un blocage permanent. Utilisez toujours try...finally.
  • Exceptions à l’intérieur de la section protégée : même si une exception survient dans le bloc, le verrou doit être libéré !
  • Utilisation excessive de ReadWriteLock : pour de petites collections, ou si l’on écrit presque tout le temps, ReadWriteLock a peu d’intérêt et complexifie le code.

8. Erreurs typiques

Erreur n° 1 : oubli d’appeler unlock()
L’erreur la plus fréquente et la plus insidieuse consiste à oublier d’appeler unlock() après l’acquisition du verrou. Résultat : blocage permanent, threads bloqués. Utilisez toujours try...finally, même si « rien ne peut casser ici ».

Erreur n° 2 : utiliser ReadWriteLock là où ce n’est pas nécessaire
S’il y a très peu de lectures parallèles et que l’écriture est fréquente, ReadWriteLock ne fera que compliquer le code et réduire les performances. Utilisez-le seulement lorsque vous avez réellement beaucoup de lecteurs simultanés.

Erreur n° 3 : acquisition de plusieurs verrous dans des ordres différents
Si votre code acquiert plusieurs Lock (par exemple, pour plusieurs objets), faites-le toujours dans le même ordre dans tous les threads. Sinon, vous risquez un deadlock — les threads se bloqueront mutuellement indéfiniment.

Erreur n° 4 : tenter de remplacer synchronized par ReentrantLock « juste comme ça »
Il ne faut pas remplacer aveuglément tous les synchronized par des Lock — cela n’accélère pas toujours le programme et peut rendre le code moins lisible.

Erreur n° 5 : oubli de la réentrance
Si un même thread appelle lock() plusieurs fois de suite, c’est normal avec ReentrantLock, mais n’oubliez pas que unlock() doit être appelé autant de fois !

1
Mission
JAVA 25 SELF, niveau 52, leçon 2
Bloqué
Gestion centrale des stocks 📦
Gestion centrale des stocks 📦
1
Mission
JAVA 25 SELF, niveau 52, leçon 2
Bloqué
Registre global de configuration de l'application ⚙️
Registre global de configuration de l'application ⚙️
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION