Tutoriel sur la programmation concurrente en Java

Partie 4 : Verrous (java.util.concurrent.locks)

Logan Mauzaize

La programmation concurrente est un enjeu important et parfois difficile pour les développeurs. Cette série d'articles vise à vous présenter les différentes API disponibles en standard avec Java.

Les articles de la série :

- Gestion des threads

- Éléments de synchronisation (natifs et API)

- Structures de données

- Verrous

- Variables sans verrou

- Conseils et astuces

Ce quatrième article vise à présenter les différents types de verrous disponibles dans l'API.

Commentez

- Titre : Tutoriel sur la programmation concurrente en Java

- Auteur : Logan Mauzaize

- Parution : 20 janvier 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Logan Mauzaize. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Bases

- II. Lock

- III. Condition

- IV. ReentrantLock

- V. ReadWriteLock

- VI. ReentrantReadWriteLock

- VII. StampedLock (1.8+)

- VIII. Conclusion

- IX. Remerciements

- X. Annexes

- X-A. Sources des exemples

- X-B. Java Concurrent Animated

I - Bases

L'API java.util.concurrent.locks est un framework de verrou similaire à l'API native. Cependant il offre une plus grande flexibilité au prix d'une syntaxe moins conviviale (notamment le fait qu'elle ne soit pas associée à des mots-clés ou des blocs).

II - Lock

L'interface de base du package est Lock (verrou). Elle définit le concept général de verrou de manière similaire à ce que peut offrir synchronized. En sus, elle offre des méthodes pour tenter d'obtenir un verrou de manière non bloquante (tryLock()), ou bien avec un temps d'attente (tryLock(long,TimeUnit)). Ces mécanismes permettent par exemple de réordonner des tâches si une ressource est occupée :

Lock - Démo (Ressource)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo

2. class Ressource {

3. class Tache {

4. // Construit une tâche qui permet de bloquer la ressource pendant le temps indiqué

5. Tache(String nom, long pause, TimeUnit unite) { /* ... */ }

6. // Tente de verrouiller la ressource, trace et renvoie le résultat

7. boolean verrouiller() { /* ... */ }

8. // Libère la ressource et trace un message

9. public void liberer() { /* ... */ }

10. }

12. // Verrou associé à la ressource

13. Lock verrou = new ReentrantLock();

14. // Construit une nouvelle ressource

15. Ressource(String nom) { /* ... */ }

16. }

Lock - Démo (Gestionnaire)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo

2. class Gestionnaire implements AutoCloseable {

4. Deque<Ressource.Tache> taches;

5. ExecutorService executor;

7. public Gestionnaire(int nbThread);

9. class ResponsableTache implements Runnable {

10. public void run() {

11. while (estActif()) {

12. Ressource.Tache suivant = null;

13. for (Iterator<Ressource.Tache> it = taches.iterator(); it.hasNext() ;) {

14. Ressource.Tache tache = it.next();

15. if (tache.verrouiller()) {

16. it.remove();

17. suivant = tache;

18. break;

19. }

20. }

21. if (suivant != null) {

22. suivant.pause();

23. suivant.liberer();

24. }

25. }

26. }

27. }

29. // Indique si le gestionnaire est actif

30. boolean estActif() { /* ... */ }

31. // Créer "nbThread" thread pour traiter les tâches

32. public void demarrer() { /* ... */ }

33. // Attend que la file de tâche soit vidée

34. public void attendre() { /* ... */ }

35. }

Lock - Démo (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo

2. Ressource a = new Ressource("A");

3. Ressource b = new Ressource("B");

4. Ressource c = new Ressource("C");

6. try (Gestionnaire gestionnaire = new Gestionnaire(2)) {

7. gestionnaire.addTache(a.new Tache("A1", 200, TimeUnit.MILLISECONDS));

8. gestionnaire.addTache(a.new Tache("A2", 200, TimeUnit.MILLISECONDS));

9. gestionnaire.addTache(b.new Tache("B1", 200, TimeUnit.MILLISECONDS));

10. gestionnaire.addTache(c.new Tache("C1", 200, TimeUnit.MILLISECONDS));

11. gestionnaire.addTache(c.new Tache("C2", 200, TimeUnit.MILLISECONDS));

12. gestionnaire.addTache(b.new Tache("B2", 200, TimeUnit.MILLISECONDS));

14. gestionnaire.demarrer();

15. gestionnaire.attendre();

16. }

Lock - Démo (console)

1. 00:00:00.076 [pool-1-thread-1] [A1] Verrouillage de A

2. 00:00:00.090 [pool-1-thread-2] [A2] Ressource A occupée

3. 00:00:00.090 [pool-1-thread-2] [B1] Verrouillage de B

4. 00:00:00.290 [pool-1-thread-1] [A1] Libération de A

5. 00:00:00.291 [pool-1-thread-1] [A2] Verrouillage de A

6. 00:00:00.291 [pool-1-thread-2] [B1] Libération de B

7. 00:00:00.292 [pool-1-thread-2] [C1] Verrouillage de C

8. 00:00:00.491 [pool-1-thread-1] [A2] Libération de A

9. 00:00:00.491 [pool-1-thread-1] [C2] Ressource C occupée

10. 00:00:00.492 [pool-1-thread-2] [C1] Libération de C

11. 00:00:00.492 [pool-1-thread-1] [B2] Verrouillage de B

12. 00:00:00.493 [pool-1-thread-2] [C2] Verrouillage de C

13. 00:00:00.693 [pool-1-thread-1] [B2] Libération de B

14. 00:00:00.694 [pool-1-thread-2] [C2] Libération de C

III - Condition

Si les « Locks » jouent le même rôle que les blocs synchronized, les Conditions jouent le même rôle que les méthodes du moniteur : wait, notify et notifyAll. Cependant, l'avantage des Conditions est la possibilité d'en avoir plusieurs distinctes pour un seul verrou. Ce qui permet de traiter différents événements sans « réveiller » TOUS les threads :

Condition - Démo (Messagerie)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.ConditionDemo

2. class Messagerie {

3. ReentrantLock verrou = new ReentrantLock();

4. Condition lecture  = verrou.newCondition();

5. Condition ecriture = verrou.newCondition();

6. Condition libre    = verrou.newCondition();

8. volatile String  envoi   = null;

9. volatile boolean occupe  = false;

11. public void envoyer(String message) {

12. verrou.lock();

14. // Occupation du canal

15. while (occupe) {

16. libre.await();

17. }

18. occupe = true;

20. // Envoi

21. envoi = message;

22. ecriture.signal();

24. // Attente de l'accusé

25. while (envoi == message) {

26. lecture.await();

27. }

29. // Libération du canal

30. occupe = false;

31. libre.signal();

33. verrou.unlock();

34. }

36. public String recevoir() {

37. verrou.lock();

39. // Attente d'un message

40. while (envoi == null) {

41. ecriture.await();

42. }

44. // Réception

45. String message = envoi;

47. // Accusation

48. envoi = null;

49. lecture.signalAll();

51. verrou.unlock();

53. return message;

54. }

55. }

Condition - Démo (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.ConditionDemo

2. final Messagerie messagerie = new Messagerie();

4. Callable<Void> emetteur  = ...; // Envoie 3 messages du type "[nom du thread] > [séquence]"

5. Callable<Void> recepteur = ...; // Reçoit 1 message et l'affichage

7. final ExecutorService executor = Executors.newCachedThreadPool();

8. List<Callable<Void>> taches = new ArrayList<>();

9. for (int i = 0; i < 2; i++) {

10. taches.add(emetteur);

11. for (int j = 0; j < 3; j++) {

12. taches.add(recepteur);

13. }

14. }

15. executor.invokeAll(taches);

16. executor.shutdown();

Condition - Démo (console)

1. 00:00:00.022 [pool-1-récepteur-6] pool-1-émetteur-5 > 0

2. 00:00:00.022 [pool-1-récepteur-4] pool-1-émetteur-1 > 1

3. 00:00:00.022 [pool-1-récepteur-8] pool-1-émetteur-1 > 0

4. 00:00:00.022 [pool-1-récepteur-3] pool-1-émetteur-1 > 2

5. 00:00:00.022 [pool-1-récepteur-2] pool-1-émetteur-5 > 2

6. 00:00:00.022 [pool-1-récepteur-7] pool-1-émetteur-5 > 1

IV - ReentrantLock

Dans les exemples précédents, nous avons utilisé l'implémentation ReentrantLock. Il s'agit d'un verrou ayant des caractéristiques similaires à l'API native. Ainsi, et comme son nom l'indique, il gère la réentrance. C'est-à-dire que le verrou est attaché au thread qui le possède. Si ce thread tente d'acquérir à nouveau le verrou, la méthode retourne immédiatement. Il est possible de vérifier si le thread courant possède le verrou grâce aux méthodes isHeldByCurrentThread() et getHoldCount(). Ce dernier permet de connaître le nombre total de demandes de verrouillage depuis que le thread a obtenu le verrou. La méthode unlock() libère le verrou, peu importe le nombre de demandes ayant été effectuées par le thread.

Il est également possible de surveiller la file d'attente grâce aux méthodes getQueueLength(), hasQueuedThreads() et hasQueuedThread(Thread).

Enfin, comme les Semaphores, ce type de verrou permet de spécifier la « parité » des attentes. Si vous utilisez le constructeur ReentrantLock(boolean) avec la valeur true alors en cas de contention, le verrou est donné prioritairement au thread qui attend depuis le plus longtemps. Ce qui permet d'éviter à un thread de rester bloquer pendant une longue période.

V - ReadWriteLock

Une autre interface proposée est ReadWriteLock. Celle-ci consiste simplement à offrir deux verrous : l'un pour la lecture (partagé) et l'autre pour l'écriture (exclusif). Ainsi le verrou en lecture peut être partagé par plusieurs threads tant qu'il n'y a pas de verrou en écriture. Si un thread acquiert le verrou en écriture, aucun thread ne peut posséder le verrou en lecture ni celui en écriture.

Ce type de structure permet de limiter les contentions en autorisant plusieurs threads à opérer une ressource simultanément tant que celle-ci ne fait pas l'objet d'une opération critique. Par exemple, il est possible à plusieurs threads de lire/parcourir une collection tant que celle-ci ne fait pas l'objet d'une modification via un verrou en lecture, tandis que le verrou en écriture assure qu'il n'y a ni écriture ni parcours (ce qui évite la réception d'une ConcurrentModificationException).

Une meilleure gestion des contentions est normalement synonyme de meilleures performances, mais cela n'est pas toujours le cas. Ainsi il convient de bien étudier vos algorithmes (et l'implémentation choisie) pour s'assurer que les performances seront meilleures. Les critères sont généralement : le rapport lecture/écriture, la durée des opérations et le niveau de concurrence. Dans tous les cas, il est recommandé de procéder à des tests représentatifs pour valider les améliorations, car si les lectures sont peu représentatives (rapides, pas si fréquentes, peu concurrentes) alors le surcoût de la gestion d'un tel verrou n'apportera pas d'amélioration des performances, voir le contraire !

VI - ReentrantReadWriteLock

La seule implémentation fournie en standard par Java est ReentrantReadWriteLock. Celle-ci dispose de différentes caractéristiques qu'il convient de connaître pour bien l'utiliser.

Comme son nom l'indique, cette implémentation gère la réentrance. Un thread qui a déjà acquis un verrou en lecture peut l'acquérir à nouveau. De même, pour un thread ayant déjà acquis le verrou en écriture. Un thread ayant le verrou en écriture peut faire l'acquisition du verrou en lecture. Les deux verrous ainsi obtenus étant distincts l'un comme l'autre, ils sont libérés de manière indépendante. Ceci permet de rétrograder (« downgrade ») le niveau du verrou (écriture -> lecture). En revanche, il n'est pas possible de surclasser (« upgrade ») le niveau du verrou (lecture -> écriture), toute tentative échouera ou bloquera indéfiniment.

Comme les Semaphores, ce type de verrou permet de spécifier la « parité » des attentes. S'il est paramétré pour ne pas appliquer de parité (« Non-fair »), comme c'est le cas par défaut, l'ordre de distribution des verrous n'est pas spécifié ; potentiellement, un thread pourrait attendre indéfiniment l'accès en lecture ou en écriture. Si la parité est activée, le verrou préserve l'ordre d'arrivée. Toutes les demandes de lectures consécutives forment un groupe, l'ensemble du groupe est débloqué d'un seul coup. Si la tête de la file est l'attente d'accès en écriture et que l'attente est abandonnée, le groupe en lecture qui suit devient alors éligible à l'accès en lecture. Si le mode courant est la lecture alors le groupe est débloqué immédiatement.

Enfin un dernier point concerne l'utilisation des conditions. Celles-ci ne concernent que le verrou en écriture. Si vous appelez la méthode readLock().newCondition(), vous obtiendrez une UnsupportedOperationException. Les conditions du verrou en écriture respectent les mêmes spécifications que ReentrantLock.

L'exemple typique de l'utilisation d'un tel verrou est la mise au point d'un cache :

ReentrantReadWriteLock - Démo (Cache)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.ReentrantReadWriteLockDemo

2. class Cache {

3. Map<Integer, Entry<String,String>> contenu = new TreeMap<>();

4. ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(parite);

6. public String get(Integer cle) {

7. verrou.readLock().lock();

8. Entry<String,String> valeur = contenu.get(cle);

9. verrou.readLock().unlock();

10. if (valeur == null) {

11. valeur = forcerChargement(cle);

12. }

13. return valeur.getKey();

14. }

16. private Entry<String,String> forcerChargement(Integer cle) {

17. verrou.writeLock().lock();

18. Entry<String,String> valeur = contenu.get(cle);

19. if (valeur == null) {

20. valeur = new SimpleImmutableEntry<>(cle.toString(), Thread.currentThread().getName());

21. contenu.put(cle, valeur);

22. Logger.println("Chargement de %d", cle);

23. } else {

24. Logger.println("Chargement annulé de %d", cle);

25. }

26. verrou.writeLock().unlock();

27. return valeur;

28. }

29. }

ReentrantReadWriteLock - Démo (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.ReentrantReadWriteLockDemo

2. Cache cache = new Cache();

3. Callable<Void> tache = new Callable<Void>() {

4. public Void call() {

5. for (int i = 0; i < 15; i++) {

6. cache.get(i);

7. }

8. return null;

9. }

10. };

11. ExecutorService executor = Executors.newCachedThreadPool();

12. executor.invokeAll(Collections.nCopies(5, tache));

13. executor.shutdown();

14. Logger.println("%s", cache);

ReentrantReadWriteLock - Démo (console)

1. 00:00:00.022 [pool-1-thread-2] Chargement de 0

2. 00:00:00.030 [pool-1-thread-1] Chargement annulé de 0

3. 00:00:00.030 [pool-1-thread-2] Chargement de 1

4. 00:00:00.031 [pool-1-thread-1] Chargement annulé de 1

5. 00:00:00.031 [pool-1-thread-3] Chargement annulé de 1

6. 00:00:00.031 [pool-1-thread-5] Chargement annulé de 1

7. 00:00:00.031 [pool-1-thread-4] Chargement annulé de 1

8. 00:00:00.032 [pool-1-thread-4] Chargement de 2

9. 00:00:00.032 [pool-1-thread-4] Chargement de 3

10. 00:00:00.032 [pool-1-thread-2] Chargement annulé de 3

11. 00:00:00.032 [pool-1-thread-3] Chargement annulé de 3

12. 00:00:00.033 [pool-1-thread-3] Chargement de 4

13. 00:00:00.033 [pool-1-thread-3] Chargement de 5

14. 00:00:00.033 [pool-1-thread-3] Chargement de 6

15. 00:00:00.033 [pool-1-thread-5] Chargement annulé de 6

16. 00:00:00.034 [pool-1-thread-1] Chargement annulé de 6

17. 00:00:00.034 [pool-1-thread-5] Chargement de 7

18. 00:00:00.034 [pool-1-thread-1] Chargement annulé de 7

19. 00:00:00.035 [pool-1-thread-4] Chargement annulé de 7

20. 00:00:00.035 [pool-1-thread-3] Chargement annulé de 7

21. 00:00:00.035 [pool-1-thread-2] Chargement annulé de 7

22. 00:00:00.035 [pool-1-thread-5] Chargement de 8

23. 00:00:00.036 [pool-1-thread-2] Chargement annulé de 8

24. 00:00:00.036 [pool-1-thread-5] Chargement de 9

25. 00:00:00.036 [pool-1-thread-1] Chargement annulé de 9

26. 00:00:00.036 [pool-1-thread-4] Chargement annulé de 9

27. 00:00:00.036 [pool-1-thread-1] Chargement de 10

28. 00:00:00.037 [pool-1-thread-4] Chargement annulé de 10

29. 00:00:00.037 [pool-1-thread-3] Chargement annulé de 10

30. 00:00:00.037 [pool-1-thread-2] Chargement annulé de 10

31. 00:00:00.037 [pool-1-thread-5] Chargement annulé de 10

32. 00:00:00.038 [pool-1-thread-4] Chargement de 11

33. 00:00:00.038 [pool-1-thread-5] Chargement annulé de 11

34. 00:00:00.038 [pool-1-thread-1] Chargement annulé de 11

35. 00:00:00.038 [pool-1-thread-4] Chargement de 12

36. 00:00:00.038 [pool-1-thread-3] Chargement annulé de 12

37. 00:00:00.039 [pool-1-thread-4] Chargement de 13

38. 00:00:00.039 [pool-1-thread-3] Chargement annulé de 13

39. 00:00:00.039 [pool-1-thread-2] Chargement annulé de 13

40. 00:00:00.039 [pool-1-thread-5] Chargement annulé de 13

41. 00:00:00.040 [pool-1-thread-4] Chargement de 14

42. 00:00:00.040 [pool-1-thread-5] Chargement annulé de 14

43. 00:00:00.040 [pool-1-thread-1] Chargement annulé de 14

44. 00:00:00.040 [main           ] {0=0=pool-1-thread-2, 1=1=pool-1-thread-2, 2=2=pool-1-thread-4, 3=3=pool-1-thread-4, 4=4=pool-1-thread-3, 5=5=pool-1-thread-3, 6=6=pool-1-thread-3, 7=7=pool-1-thread-5, 8=8=pool-1-thread-5, 9=9=pool-1-thread-5, 10=10=pool-1-thread-1, 11=11=pool-1-thread-4, 12=12=pool-1-thread-4, 13=13=pool-1-thread-4, 14=14=pool-1-thread-4}

VII - StampedLock (1.8+)

Un autre type de verrou disponible (depuis Java 8) est StampedLock. Ce type de verrou n'est pas réentrant et n'est donc pas attaché à un thread en particulier. En revanche, il utilise un tampon (« stamp ») pour valider les changements d'état.

StampedLock - Démo (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockDemo

2. class Contexte {

3. StampedLock verrou = new StampedLock();

4. long tampon;

6. // Exécute l'action dans un nouveau thread avec le nom donné

7. void executer(String nom, Runnable action) { /* ... */ }

8. }

9. Contexte c = new Contexte();

11. c.executer("readLock", () -> {

12. c.tampon = c.verrou.readLock();

13. Logger.println("Verrouiller en lecture (%d)", c.tampon);

14. });

16. c.executer("invalidUnlock", () -> {

17. long tampon = c.tampon-1;

18. try {

19. c.verrou.unlock(tampon);

20. } catch (IllegalMonitorStateException e) {

21. Logger.println("Impossible de déverrouiller (%d)", tampon);

22. }

23. });

25. c.executer("unlockWrite", () -> {

26. try {

27. c.verrou.unlockWrite(c.tampon);

28. } catch  (IllegalMonitorStateException e) {

29. Logger.println("Impossible de déverrouiller en écriture (%d)", c.tampon);

30. }

31. });

33. c.executer("unlock", () -> {

34. c.verrou.unlock(c.tampon);

35. Logger.println("Déverrouiller (%d)", c.tampon);

36. });

StampedLock - Démo (console)

1. 00:00:00.024 [readLock       ] Verrouiller en lecture (257)

2. 00:00:00.033 [invalidUnlock  ] Impossible de déverrouiller (256)

3. 00:00:00.034 [unlockWrite    ] Impossible de déverrouiller en écriture (257)

4. 00:00:00.035 [unlock         ] Déverrouiller (257)

De manière similaire au ReentrantReadWriteLock, les StampedLock offrent un verrou partagé (readLock()) et un verrou exclusif (writeLock()) sous la forme de « mode ». Pour chaque « mode », il existe une série de méthodes :

- is*Locked() (read/write) : vérifie le « mode » actuel du verrou.

- *Lock() (read/write) : attend que le « mode » demandé soit disponible, puis renvoie un tampon.

- *LockInterruptibly() (read/write) : même chose que précédemment, mais peut éventuellement lever une InterruptedException.

- try*Lock() (read/write) : vérifie si le « mode » demandé est disponible. Renvoie 0 si ce n'est pas le cas.

- try*Lock(long,TimeUnit) (read/write) : même chose que précédemment, mais permet d'attendre pendant un certains laps de temps.

- tryUnlock*() (read/write) : ces méthodes permettent de débloquer une seule prise de verrou.

- unlock*(long) (read/write) : débloque le verrou si le « mode » et le tampon correspondent.

S'ils n'implémentent ni l'interface Lock, ni ReadWriteLock, ils offrent cependant les méthodes suivantes :

- asReadLock() : vue implémentant l'interface Lock. Les méthodes respectent les règles du « mode » partagé. Il n'est pas possible de créer de Condition.

- asWriteLock() : même chose que précédemment, mais pour le « mode » exclusif.

- asReadWriteLock() : vue implémentant l'interface ReadWriteLock. Les méthodes readLock() et writeLock() correspondent aux méthodes asReadLock() et asWriteLock().

StampedLock - Read/Write lock (CompteBancaire)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockReadWriteLock

2. class CompteBancaire {

3. StampedLock verrou = new StampedLock();

4. long solde = 0;

6. void modifier(long montant) {

7. long tampon = verrou.writeLock();

8. solde += montant;

9. verrou.unlockWrite(tampon);

10. }

12. long consulter() {

13. long tampon = verrou.readLock();

14. long consultation = solde;

15. verrou.unlockRead(tampon);

16. return consultation;

17. }

18. }

StampedLock - Read/Write lock (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockReadWriteLock

2. final CompteBancaire compte = new CompteBancaire();

4. Runnable curieu = () -> {

5. while (true) {

6. compte.consulter();

7. Thread.yield();

8. }

9. };

10. for (int i = 0; i < 2; i++) {

11. Thread thread = new Thread(curieu, "Curieux-" + i);

12. thread.setDaemon(true);

13. thread.start();

14. }

17. Callable<long[]> operateur = () -> {

18. long[] montants = new Random().longs(-300, 700).limit(256).toArray();

19. for (long montant : montants) {

20. compte.modifier(montant);

21. Thread.yield();

22. }

23. return montants;

24. };

25. ExecutorService executeur = Executors.newCachedThreadPool();

26. List<Future<long[]>>   resultats = executeur.invokeAll(Collections.nCopies(4, opérateur));

27. exécuteur.shutdown();

29. long solde = 0;

30. for (Future<long[]> résultat : résultats) {

31. for (long montant : résultat.get()) {

32. solde += montant;

33. }

34. }

35. Logger.println("Solde réel=%d, attendu=%d", compte.consulter(), solde);

StampedLock - Read/Write lock (console)

1. 00:00:00.030 [main           ] Solde réel=199260, attendu=199260

L'un des avantages d'un StampedLock est de permettre la lecture optimiste via la méthode tryOptimisticRead(). Celle-ci renvoie un tampon (éventuellement non valide) ; on peut ainsi vérifier ultérieurement si une écriture a eu lieu via la méthode validate().

StampedLock - Lecture optimiste (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockLectureOptimiste

2. StampedLock verrou = new StampedLock();

4. long optimiste = verrou.tryOptimisticRead();

5. Logger.println("Essai de lecture optimiste (%s)", optimiste);

6. Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));

8. Logger.println("");

10. long lecture   = verrou.tryReadLock();

11. Logger.println("Essai de lecture (%s)", lecture);

12. Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));

13. Logger.println("Validité (%s) ? %s", lecture  , verrou.validate(lecture));

15. Logger.println("");

17. Logger.println("Essai d'écriture (%s)", verrou.tryWriteLock());

18. Logger.println("Validité (%s) ? %s"   , optimiste, verrou.validate(optimiste));

19. Logger.println("Validité (%s) ? %s"   , lecture  , verrou.validate(lecture));

21. Logger.println("");

23. Logger.println("Déverrouillage en lecture de (%s)", lecture);

24. verrou.unlockRead(lecture);

25. Logger.println("Validité (%s) ? %s"   , optimiste, verrou.validate(optimiste));

26. Logger.println("Validité (%s) ? %s"   , lecture  , verrou.validate(lecture));

28. Logger.println("");

30. long ecriture  = verrou.tryWriteLock();

31. Logger.println("Essai d'écriture (%s)", ecriture);

32. Logger.println("Validité (%s) ? %s"   , optimiste, verrou.validate(optimiste));

33. Logger.println("Validité (%s) ? %s"   , lecture  , verrou.validate(lecture));

35. Logger.println("");

37. Logger.println("Essai de lecture optimiste (%s)", verrou.tryOptimisticRead());

39. Logger.println("");

41. Logger.println("Déverrouillage en écriture de (%s)", ecriture);

42. verrou.unlockWrite(ecriture);

43. Logger.println("Validité (%s) ? %s"   , optimiste, verrou.validate(optimiste));

44. Logger.println("Validité (%s) ? %s"   , lecture  , verrou.validate(lecture));

46. Logger.println("");

48. optimiste = verrou.tryOptimisticRead();

49. Logger.println("Essai de lecture optimiste (%s)", optimiste);

50. Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));

51. Logger.println("Validité (%s) ? %s", lecture  , verrou.validate(lecture));

StampedLock - Lecture optimiste (console)

1. 00:00:00.036 [main           ] Essai de lecture optimiste (256)

2. 00:00:00.045 [main           ] Validité (256) ? true

3. 00:00:00.045 [main           ]

4. 00:00:00.046 [main           ] Essai de lecture (257)

5. 00:00:00.046 [main           ] Validité (256) ? true

6. 00:00:00.046 [main           ] Validité (257) ? true

7. 00:00:00.047 [main           ]

8. 00:00:00.047 [main           ] Essai d'écriture (0)

9. 00:00:00.047 [main           ] Validité (256) ? true

10. 00:00:00.048 [main           ] Validité (257) ? true

11. 00:00:00.048 [main           ]

12. 00:00:00.048 [main           ] Déverrouillage en lecture de (257)

13. 00:00:00.048 [main           ] Validité (256) ? true

14. 00:00:00.049 [main           ] Validité (257) ? true

15. 00:00:00.049 [main           ]

16. 00:00:00.049 [main           ] Essai d'écriture (384)

17. 00:00:00.049 [main           ] Validité (256) ? false

18. 00:00:00.050 [main           ] Validité (257) ? false

19. 00:00:00.050 [main           ]

20. 00:00:00.050 [main           ] Essai de lecture optimiste (0)

21. 00:00:00.050 [main           ]

22. 00:00:00.050 [main           ] Déverrouillage en écriture de (384)

23. 00:00:00.051 [main           ] Validité (256) ? false

24. 00:00:00.051 [main           ] Validité (257) ? false

25. 00:00:00.051 [main           ]

26. 00:00:00.051 [main           ] Essai de lecture optimiste (512)

27. 00:00:00.052 [main           ] Validité (512) ? true

28. 00:00:00.052 [main           ] Validité (257) ? false

L'un des avantages des StampedLock, c'est qu'ils permettent de rétrograder (« downgrade ») et de surclasser (« upgrade ») le verrou en modifiant son mode. Pour cela, les méthodes ci-dessous sont proposées. Elles ont toutes la particularité de renvoyer un nouveau tampon à utiliser par la suite ; celui-ci prend alors la valeur 0 si l'opération n'est pas possible.

- tryConvertToOptimisticRead : si le tampon correspond à un verrou alors le retire, puis renvoie un tampon de lecture optimiste (qui pourra être validé par la suite). Si le tampon correspond déjà à une lecture optimiste, le renvoie si aucun verrou n'a été posé. Dans tous les autres cas, la conversion échoue et renvoie 0.

StampedLock - Conversion en lecture optimiste (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionOptimiste

2. StampedLock verrou = new StampedLock();

4. long optimiste = verrou.tryOptimisticRead();

5. Logger.println("Lecture optimiste (%s)", optimiste);

6. Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste));

8. Logger.println("");

10. long lecture = verrou.tryReadLock();

11. Logger.println("Lecture (%s)", lecture);

12. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

13. Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));

14. Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste));

15. optimiste = verrou.tryConvertToOptimisticRead(lecture);

16. Logger.println("Conversion en lecture optimiste de (%s) en (%s)", lecture, optimiste);

17. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

19. Logger.println("");

21. long ecriture = verrou.tryWriteLock();

22. Logger.println("Écriture (%s)", ecriture);

23. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

24. Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));

25. Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste));

26. optimiste = verrou.tryConvertToOptimisticRead(ecriture);

27. Logger.println("Conversion en lecture optimiste de (%s) en (%s)", ecriture, optimiste);

28. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

StampedLock - Conversion en lecture optimiste (console)

1. 00:00:00.146 [main           ] Lecture optimiste (256)

2. 00:00:00.408 [main           ] Conversion en lecture optimiste de (256) en (256)

3. 00:00:00.408 [main           ]

4. 00:00:00.409 [main           ] Lecture (257)

5. 00:00:00.409 [main           ] Verrous lecture(1) écriture(0)

6. 00:00:00.410 [main           ] Validité (256) ? true

7. 00:00:00.412 [main           ] Conversion en lecture optimiste de (256) en (0)

8. 00:00:00.412 [main           ] Conversion en lecture optimiste de (257) en (256)

9. 00:00:00.413 [main           ] Verrous lecture(0) écriture(0)

10. 00:00:00.413 [main           ]

11. 00:00:00.414 [main           ] Écriture (384)

12. 00:00:00.414 [main           ] Verrous lecture(0) écriture(1)

13. 00:00:00.415 [main           ] Validité (256) ? false

14. 00:00:00.416 [main           ] Conversion en lecture optimiste de (256) en (0)

15. 00:00:00.416 [main           ] Conversion en lecture optimiste de (384) en (512)

16. 00:00:00.417 [main           ] Verrous lecture(0) écriture(0)

- tryConvertToReadLock : si le tampon correspond à un verrou en écriture, alors libère le verrou en écriture, obtient un verrou en lecture et renvoie un nouveau tampon. S'il s'agit déjà d'un verrou en lecture, renvoie le tampon. S'il s'agit d'une lecture optimiste, alors il essaie d'obtenir un verrou en lecture et renvoie un nouveau tampon. Dans tous les autres cas, la demande est un échec et renvoie 0.

StampedLock - Conversion en lecture (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionLecture

2. StampedLock verrou = new StampedLock();

4. long optimiste = verrou.tryOptimisticRead();

5. Logger.println("Lecture optimiste (%s)", optimiste);

6. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

7. long lecture1 = verrou.tryConvertToReadLock(optimiste);

8. Logger.println("Conversion en lecture de (%s) en (%s)", optimiste, lecture1);

9. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

11. Logger.println("");

13. long lecture2 = verrou.tryReadLock();

14. Logger.println("Lecture (%s)", lecture2);

15. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

16. Logger.println("Conversion en lecture de (%s) en (%s)", lecture2, verrou.tryConvertToReadLock(lecture2));

17. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

19. Logger.println("");

21. Logger.println("Libération lecture (%s, %s)", lecture1, lecture2);

22. verrou.unlockRead(lecture1);

23. verrou.unlockRead(lecture2);

24. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

25. long ecriture = verrou.tryWriteLock();

26. Logger.println("Écriture (%s)", ecriture);

27. Logger.println("Conversion en lecture de (%s) en (%s)", ecriture, verrou.tryConvertToReadLock(ecriture));

28. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

StampedLock - Conversion en lecture (console)

1. 00:00:00.022 [main           ] Lecture optimiste (256)

2. 00:00:00.030 [main           ] Verrous lecture(0) écriture(0)

3. 00:00:00.030 [main           ] Conversion en lecture de (256) en (257)

4. 00:00:00.031 [main           ] Verrous lecture(1) écriture(0)

5. 00:00:00.031 [main           ]

6. 00:00:00.031 [main           ] Lecture (258)

7. 00:00:00.031 [main           ] Verrous lecture(2) écriture(0)

8. 00:00:00.032 [main           ] Conversion en lecture de (258) en (258)

9. 00:00:00.032 [main           ] Verrous lecture(2) écriture(0)

10. 00:00:00.032 [main           ]

11. 00:00:00.032 [main           ] Libération lecture (257, 258)

12. 00:00:00.033 [main           ] Verrous lecture(0) écriture(0)

13. 00:00:00.033 [main           ] Écriture (384)

14. 00:00:00.033 [main           ] Conversion en lecture de (384) en (513)

15. 00:00:00.034 [main           ] Verrous lecture(1) écriture(0)

- tryConvertToWriteLock : s'il s'agit d'un verrou en écriture, renvoie le tampon. S'il s'agit d'un verrou en lecture et que le verrouillage en écriture est possible alors libère le verrou en lecture, obtient le verrou en écriture et renvoie un nouveau tampon. S'il s'agit d'un tampon de lecture optimiste, alors renvoie un tampon en écriture, si le verrouillage en écriture est possible. Dans tous les autres cas, la conversion échoue et renvoie 0.

StampedLock - Conversion en écriture (code)

1. //com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionEcriture

2. StampedLock verrou = new StampedLock();

4. long optimiste = verrou.tryOptimisticRead();

5. Logger.println("Lecture optimiste (%s)", optimiste);

6. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

7. long ecriture = verrou.tryConvertToWriteLock(optimiste);

8. Logger.println("Conversion en écriture de (%s) en (%s)", optimiste, ecriture);

9. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

10. Logger.println("Libération écriture (%s)", ecriture);

11. verrou.unlockWrite(ecriture);

12. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

14. Logger.println("");

16. long lecture1 = verrou.tryReadLock();

17. Logger.println("Lecture (%s)", lecture1);

18. long lecture2 = verrou.tryReadLock();

19. Logger.println("Lecture (%s)", lecture2);

20. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

21. écriture = verrou.tryConvertToWriteLock(lecture1);

22. Logger.println("Conversion en écriture de (%s) en (%s)", lecture1, ecriture);

23. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

25. Logger.println("");

27. Logger.println("Libération écriture (%s)", lecture2);

28. verrou.unlockRead(lecture2);

29. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

30. écriture = verrou.tryConvertToWriteLock(lecture1);

31. Logger.println("Conversion en écriture de (%s) en (%s)", lecture1, ecriture);

32. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

34. Logger.println("");

36. Logger.println("Conversion en écriture de (%s) en (%s)", ecriture, verrou.tryConvertToWriteLock(ecriture));

37. Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");

StampedLock - Conversion en écriture (console)

1. 00:00:00.022 [main           ] Lecture optimiste (256)

2. 00:00:00.030 [main           ] Verrous lecture(0) écriture(0)

3. 00:00:00.030 [main           ] Conversion en écriture de (256) en (384)

4. 00:00:00.031 [main           ] Verrous lecture(0) écriture(1)

5. 00:00:00.031 [main           ] Libération écriture (384)

6. 00:00:00.031 [main           ] Verrous lecture(0) écriture(0)

7. 00:00:00.032 [main           ]

8. 00:00:00.032 [main           ] Lecture (513)

9. 00:00:00.032 [main           ] Lecture (514)

10. 00:00:00.032 [main           ] Verrous lecture(2) écriture(0)

11. 00:00:00.033 [main           ] Conversion en écriture de (513) en (0)

12. 00:00:00.033 [main           ] Verrous lecture(2) écriture(0)

13. 00:00:00.033 [main           ]

14. 00:00:00.034 [main           ] Libération écriture (514)

15. 00:00:00.034 [main           ] Verrous lecture(1) écriture(0)

16. 00:00:00.034 [main           ] Conversion en écriture de (513) en (640)

17. 00:00:00.034 [main           ] Verrous lecture(0) écriture(1)

18. 00:00:00.035 [main           ]

19. 00:00:00.035 [main           ] Conversion en écriture de (640) en (640)

20. 00:00:00.035 [main           ] Verrous lecture(0) écriture(1)

VIII - Conclusion

Au cours de ce quatrième article, nous avons vu comment gérer le blocage de ressources d'abord de manière simple (similaire à l'API native), puis de manière plus évoluée pour limiter les blocages inutiles. Le prochain article sera dédié aux variables sans blocage (lock-free).

IX - Remerciements

Je remercie tous les contributeurs de l'API Java et les packages java.util.concurrent.* et plus particulièrement Doug Lea qui est l'un des principaux auteurs.

Je remercie également Mickael Baron, Thierry Leriche-Dessirier alias thierryler, Claude Leloup et f-leb pour leur relecture attentive, leurs remarques et leurs bons conseils.

Je tiens aussi à remercier la communauté Developpez.com qui a mis en place tous les outils, procédures et l'hébergement nécessaires à la publication de cet article.

Enfin mon épouse et mes enfants pour leur patience et leur tolérance durant les nombreuses heures qui ont été nécessaires à la rédaction de cet article.

X - Annexes

X-A - Sources des exemples

Tous les exemples donnés dans cet article sont disponibles sous la forme d'un projet Maven hébergé sous GitHub. Tous les exemples cités contiennent une première ligne commentaire indiquant l'emplacement du fichier dans les sources. Les sources propres à ce chapitre se trouvent sous le package com.developpez.lmauzaize.java.concurrence.ch04_verrous.

Si vous ne savez pas comment importer le projet, je vous invite à consulter l'article « Importer un projet Maven dans Eclipse en 5 minutes ».

X-B - Java Concurrent Animated

Java Concurrent Animated est un projet Swing visant à montrer graphiquement le comportement de différents composants de l'API concurrente de Java.