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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo
class Ressource {
class Tache {
// Construit une tâche qui permet de bloquer la ressource pendant le temps indiqué
Tache(String nom, long pause, TimeUnit unite) { /* ... */ }
// Tente de verrouiller la ressource, trace et renvoie le résultat
boolean verrouiller() { /* ... */ }
// Libère la ressource et trace un message
public void liberer() { /* ... */ }
}
// Verrou associé à la ressource
Lock verrou = new ReentrantLock();
// Construit une nouvelle ressource
Ressource(String nom) { /* ... */ }
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo
class Gestionnaire implements AutoCloseable {
Deque<Ressource.Tache> taches;
ExecutorService executor;
public Gestionnaire(int nbThread);
class ResponsableTache implements Runnable {
public void run() {
while (estActif()) {
Ressource.Tache suivant = null;
for (Iterator<Ressource.Tache> it = taches.iterator(); it.hasNext() ;) {
Ressource.Tache tache = it.next();
if (tache.verrouiller()) {
it.remove();
suivant = tache;
break;
}
}
if (suivant != null) {
suivant.pause();
suivant.liberer();
}
}
}
}
// Indique si le gestionnaire est actif
boolean estActif() { /* ... */ }
// Créer "nbThread" thread pour traiter les tâches
public void demarrer() { /* ... */ }
// Attend que la file de tâche soit vidée
public void attendre() { /* ... */ }
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.LockDemo
Ressource a = new Ressource("A");
Ressource b = new Ressource("B");
Ressource c = new Ressource("C");
try (Gestionnaire gestionnaire = new Gestionnaire(2)) {
gestionnaire.addTache(a.new Tache("A1", 200, TimeUnit.MILLISECONDS));
gestionnaire.addTache(a.new Tache("A2", 200, TimeUnit.MILLISECONDS));
gestionnaire.addTache(b.new Tache("B1", 200, TimeUnit.MILLISECONDS));
gestionnaire.addTache(c.new Tache("C1", 200, TimeUnit.MILLISECONDS));
gestionnaire.addTache(c.new Tache("C2", 200, TimeUnit.MILLISECONDS));
gestionnaire.addTache(b.new Tache("B2", 200, TimeUnit.MILLISECONDS));
gestionnaire.demarrer();
gestionnaire.attendre();
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
00:00:00.076 [pool-1-thread-1] [A1] Verrouillage de A
00:00:00.090 [pool-1-thread-2] [A2] Ressource A occupée
00:00:00.090 [pool-1-thread-2] [B1] Verrouillage de B
00:00:00.290 [pool-1-thread-1] [A1] Libération de A
00:00:00.291 [pool-1-thread-1] [A2] Verrouillage de A
00:00:00.291 [pool-1-thread-2] [B1] Libération de B
00:00:00.292 [pool-1-thread-2] [C1] Verrouillage de C
00:00:00.491 [pool-1-thread-1] [A2] Libération de A
00:00:00.491 [pool-1-thread-1] [C2] Ressource C occupée
00:00:00.492 [pool-1-thread-2] [C1] Libération de C
00:00:00.492 [pool-1-thread-1] [B2] Verrouillage de B
00:00:00.493 [pool-1-thread-2] [C2] Verrouillage de C
00:00:00.693 [pool-1-thread-1] [B2] Libération de B
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.ConditionDemo
class Messagerie {
ReentrantLock verrou = new ReentrantLock();
Condition lecture = verrou.newCondition();
Condition ecriture = verrou.newCondition();
Condition libre = verrou.newCondition();
volatile String envoi = null;
volatile boolean occupe = false;
public void envoyer(String message) {
verrou.lock();
// Occupation du canal
while (occupe) {
libre.await();
}
occupe = true;
// Envoi
envoi = message;
ecriture.signal();
// Attente de l'accusé
while (envoi == message) {
lecture.await();
}
// Libération du canal
occupe = false;
libre.signal();
verrou.unlock();
}
public String recevoir() {
verrou.lock();
// Attente d'un message
while (envoi == null) {
ecriture.await();
}
// Réception
String message = envoi;
// Accusation
envoi = null;
lecture.signalAll();
verrou.unlock();
return message;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.ConditionDemo
final Messagerie messagerie = new Messagerie();
Callable<Void> emetteur = ...; // Envoie 3 messages du type "[nom du thread] > [séquence]"
Callable<Void> recepteur = ...; // Reçoit 1 message et l'affichage
final ExecutorService executor = Executors.newCachedThreadPool();
List<Callable<Void>> taches = new ArrayList<>();
for (int i = 0; i < 2; i++) {
taches.add(emetteur);
for (int j = 0; j < 3; j++) {
taches.add(recepteur);
}
}
executor.invokeAll(taches);
executor.shutdown();
2.
3.
4.
5.
6.
00:00:00.022 [pool-1-récepteur-6] pool-1-émetteur-5 > 0
00:00:00.022 [pool-1-récepteur-4] pool-1-émetteur-1 > 1
00:00:00.022 [pool-1-récepteur-8] pool-1-émetteur-1 > 0
00:00:00.022 [pool-1-récepteur-3] pool-1-émetteur-1 > 2
00:00:00.022 [pool-1-récepteur-2] pool-1-émetteur-5 > 2
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.ReentrantReadWriteLockDemo
class Cache {
Map<Integer, Entry<String,String>> contenu = new TreeMap<>();
ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(parite);
public String get(Integer cle) {
verrou.readLock().lock();
Entry<String,String> valeur = contenu.get(cle);
verrou.readLock().unlock();
if (valeur == null) {
valeur = forcerChargement(cle);
}
return valeur.getKey();
}
private Entry<String,String> forcerChargement(Integer cle) {
verrou.writeLock().lock();
Entry<String,String> valeur = contenu.get(cle);
if (valeur == null) {
valeur = new SimpleImmutableEntry<>(cle.toString(), Thread.currentThread().getName());
contenu.put(cle, valeur);
Logger.println("Chargement de %d", cle);
} else {
Logger.println("Chargement annulé de %d", cle);
}
verrou.writeLock().unlock();
return valeur;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.ReentrantReadWriteLockDemo
Cache cache = new Cache();
Callable<Void> tache = new Callable<Void>() {
public Void call() {
for (int i = 0; i < 15; i++) {
cache.get(i);
}
return null;
}
};
ExecutorService executor = Executors.newCachedThreadPool();
executor.invokeAll(Collections.nCopies(5, tache));
executor.shutdown();
Logger.println("%s", cache);
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
00:00:00.022 [pool-1-thread-2] Chargement de 0
00:00:00.030 [pool-1-thread-1] Chargement annulé de 0
00:00:00.030 [pool-1-thread-2] Chargement de 1
00:00:00.031 [pool-1-thread-1] Chargement annulé de 1
00:00:00.031 [pool-1-thread-3] Chargement annulé de 1
00:00:00.031 [pool-1-thread-5] Chargement annulé de 1
00:00:00.031 [pool-1-thread-4] Chargement annulé de 1
00:00:00.032 [pool-1-thread-4] Chargement de 2
00:00:00.032 [pool-1-thread-4] Chargement de 3
00:00:00.032 [pool-1-thread-2] Chargement annulé de 3
00:00:00.032 [pool-1-thread-3] Chargement annulé de 3
00:00:00.033 [pool-1-thread-3] Chargement de 4
00:00:00.033 [pool-1-thread-3] Chargement de 5
00:00:00.033 [pool-1-thread-3] Chargement de 6
00:00:00.033 [pool-1-thread-5] Chargement annulé de 6
00:00:00.034 [pool-1-thread-1] Chargement annulé de 6
00:00:00.034 [pool-1-thread-5] Chargement de 7
00:00:00.034 [pool-1-thread-1] Chargement annulé de 7
00:00:00.035 [pool-1-thread-4] Chargement annulé de 7
00:00:00.035 [pool-1-thread-3] Chargement annulé de 7
00:00:00.035 [pool-1-thread-2] Chargement annulé de 7
00:00:00.035 [pool-1-thread-5] Chargement de 8
00:00:00.036 [pool-1-thread-2] Chargement annulé de 8
00:00:00.036 [pool-1-thread-5] Chargement de 9
00:00:00.036 [pool-1-thread-1] Chargement annulé de 9
00:00:00.036 [pool-1-thread-4] Chargement annulé de 9
00:00:00.036 [pool-1-thread-1] Chargement de 10
00:00:00.037 [pool-1-thread-4] Chargement annulé de 10
00:00:00.037 [pool-1-thread-3] Chargement annulé de 10
00:00:00.037 [pool-1-thread-2] Chargement annulé de 10
00:00:00.037 [pool-1-thread-5] Chargement annulé de 10
00:00:00.038 [pool-1-thread-4] Chargement de 11
00:00:00.038 [pool-1-thread-5] Chargement annulé de 11
00:00:00.038 [pool-1-thread-1] Chargement annulé de 11
00:00:00.038 [pool-1-thread-4] Chargement de 12
00:00:00.038 [pool-1-thread-3] Chargement annulé de 12
00:00:00.039 [pool-1-thread-4] Chargement de 13
00:00:00.039 [pool-1-thread-3] Chargement annulé de 13
00:00:00.039 [pool-1-thread-2] Chargement annulé de 13
00:00:00.039 [pool-1-thread-5] Chargement annulé de 13
00:00:00.040 [pool-1-thread-4] Chargement de 14
00:00:00.040 [pool-1-thread-5] Chargement annulé de 14
00:00:00.040 [pool-1-thread-1] Chargement annulé de 14
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockDemo
class Contexte {
StampedLock verrou = new StampedLock();
long tampon;
// Exécute l'action dans un nouveau thread avec le nom donné
void executer(String nom, Runnable action) { /* ... */ }
}
Contexte c = new Contexte();
c.executer("readLock", () -> {
c.tampon = c.verrou.readLock();
Logger.println("Verrouiller en lecture (%d)", c.tampon);
});
c.executer("invalidUnlock", () -> {
long tampon = c.tampon-1;
try {
c.verrou.unlock(tampon);
} catch (IllegalMonitorStateException e) {
Logger.println("Impossible de déverrouiller (%d)", tampon);
}
});
c.executer("unlockWrite", () -> {
try {
c.verrou.unlockWrite(c.tampon);
} catch (IllegalMonitorStateException e) {
Logger.println("Impossible de déverrouiller en écriture (%d)", c.tampon);
}
});
c.executer("unlock", () -> {
c.verrou.unlock(c.tampon);
Logger.println("Déverrouiller (%d)", c.tampon);
});
2.
3.
4.
00:00:00.024 [readLock ] Verrouiller en lecture (257)
00:00:00.033 [invalidUnlock ] Impossible de déverrouiller (256)
00:00:00.034 [unlockWrite ] Impossible de déverrouiller en écriture (257)
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().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockReadWriteLock
class CompteBancaire {
StampedLock verrou = new StampedLock();
long solde = 0;
void modifier(long montant) {
long tampon = verrou.writeLock();
solde += montant;
verrou.unlockWrite(tampon);
}
long consulter() {
long tampon = verrou.readLock();
long consultation = solde;
verrou.unlockRead(tampon);
return consultation;
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockReadWriteLock
final CompteBancaire compte = new CompteBancaire();
Runnable curieu = () -> {
while (true) {
compte.consulter();
Thread.yield();
}
};
for (int i = 0; i < 2; i++) {
Thread thread = new Thread(curieu, "Curieux-" + i);
thread.setDaemon(true);
thread.start();
}
Callable<long[]> operateur = () -> {
long[] montants = new Random().longs(-300, 700).limit(256).toArray();
for (long montant : montants) {
compte.modifier(montant);
Thread.yield();
}
return montants;
};
ExecutorService executeur = Executors.newCachedThreadPool();
List<Future<long[]>> resultats = executeur.invokeAll(Collections.nCopies(4, opérateur));
exécuteur.shutdown();
long solde = 0;
for (Future<long[]> résultat : résultats) {
for (long montant : résultat.get()) {
solde += montant;
}
}
Logger.println("Solde réel=%d, attendu=%d", compte.consulter(), solde);
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().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockLectureOptimiste
StampedLock verrou = new StampedLock();
long optimiste = verrou.tryOptimisticRead();
Logger.println("Essai de lecture optimiste (%s)", optimiste);
Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));
Logger.println("");
long lecture = verrou.tryReadLock();
Logger.println("Essai de lecture (%s)", lecture);
Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s", lecture , verrou.validate(lecture));
Logger.println("");
Logger.println("Essai d'écriture (%s)", verrou.tryWriteLock());
Logger.println("Validité (%s) ? %s" , optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s" , lecture , verrou.validate(lecture));
Logger.println("");
Logger.println("Déverrouillage en lecture de (%s)", lecture);
verrou.unlockRead(lecture);
Logger.println("Validité (%s) ? %s" , optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s" , lecture , verrou.validate(lecture));
Logger.println("");
long ecriture = verrou.tryWriteLock();
Logger.println("Essai d'écriture (%s)", ecriture);
Logger.println("Validité (%s) ? %s" , optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s" , lecture , verrou.validate(lecture));
Logger.println("");
Logger.println("Essai de lecture optimiste (%s)", verrou.tryOptimisticRead());
Logger.println("");
Logger.println("Déverrouillage en écriture de (%s)", ecriture);
verrou.unlockWrite(ecriture);
Logger.println("Validité (%s) ? %s" , optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s" , lecture , verrou.validate(lecture));
Logger.println("");
optimiste = verrou.tryOptimisticRead();
Logger.println("Essai de lecture optimiste (%s)", optimiste);
Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste));
Logger.println("Validité (%s) ? %s", lecture , verrou.validate(lecture));
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
00:00:00.036 [main ] Essai de lecture optimiste (256)
00:00:00.045 [main ] Validité (256) ? true
00:00:00.045 [main ]
00:00:00.046 [main ] Essai de lecture (257)
00:00:00.046 [main ] Validité (256) ? true
00:00:00.046 [main ] Validité (257) ? true
00:00:00.047 [main ]
00:00:00.047 [main ] Essai d'écriture (0)
00:00:00.047 [main ] Validité (256) ? true
00:00:00.048 [main ] Validité (257) ? true
00:00:00.048 [main ]
00:00:00.048 [main ] Déverrouillage en lecture de (257)
00:00:00.048 [main ] Validité (256) ? true
00:00:00.049 [main ] Validité (257) ? true
00:00:00.049 [main ]
00:00:00.049 [main ] Essai d'écriture (384)
00:00:00.049 [main ] Validité (256) ? false
00:00:00.050 [main ] Validité (257) ? false
00:00:00.050 [main ]
00:00:00.050 [main ] Essai de lecture optimiste (0)
00:00:00.050 [main ]
00:00:00.050 [main ] Déverrouillage en écriture de (384)
00:00:00.051 [main ] Validité (256) ? false
00:00:00.051 [main ] Validité (257) ? false
00:00:00.051 [main ]
00:00:00.051 [main ] Essai de lecture optimiste (512)
00:00:00.052 [main ] Validité (512) ? true
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)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionOptimisteStampedLock verrou=newStampedLock();longoptimiste=verrou.tryOptimisticRead(); Logger.println("Lecture optimiste (%s)", optimiste); Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste)); Logger.println("");longlecture=verrou.tryReadLock(); Logger.println("Lecture (%s)", lecture); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste)); Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste)); optimiste=verrou.tryConvertToOptimisticRead(lecture); Logger.println("Conversion en lecture optimiste de (%s) en (%s)", lecture, optimiste); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println("");longecriture=verrou.tryWriteLock(); Logger.println("Écriture (%s)", ecriture); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println("Validité (%s) ? %s", optimiste, verrou.validate(optimiste)); Logger.println("Conversion en lecture optimiste de (%s) en (%s)", optimiste, verrou.tryConvertToOptimisticRead(optimiste)); optimiste=verrou.tryConvertToOptimisticRead(ecriture); Logger.println("Conversion en lecture optimiste de (%s) en (%s)", ecriture, optimiste); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0");StampedLock - Conversion en lecture optimiste (console)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.00:00:00.146 [main ] Lecture optimiste (256) 00:00:00.408 [main ] Conversion en lecture optimiste de (256) en (256) 00:00:00.408 [main ] 00:00:00.409 [main ] Lecture (257) 00:00:00.409 [main ] Verrous lecture(1) écriture(0) 00:00:00.410 [main ] Validité (256) ? true 00:00:00.412 [main ] Conversion en lecture optimiste de (256) en (0) 00:00:00.412 [main ] Conversion en lecture optimiste de (257) en (256) 00:00:00.413 [main ] Verrous lecture(0) écriture(0) 00:00:00.413 [main ] 00:00:00.414 [main ] Écriture (384) 00:00:00.414 [main ] Verrous lecture(0) écriture(1) 00:00:00.415 [main ] Validité (256) ? false 00:00:00.416 [main ] Conversion en lecture optimiste de (256) en (0) 00:00:00.416 [main ] Conversion en lecture optimiste de (384) en (512) 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)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionLectureStampedLock verrou=newStampedLock();longoptimiste=verrou.tryOptimisticRead(); Logger.println("Lecture optimiste (%s)", optimiste); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0");longlecture1=verrou.tryConvertToReadLock(optimiste); Logger.println("Conversion en lecture de (%s) en (%s)", optimiste, lecture1); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println("");longlecture2=verrou.tryReadLock(); Logger.println("Lecture (%s)", lecture2); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println("Conversion en lecture de (%s) en (%s)", lecture2, verrou.tryConvertToReadLock(lecture2)); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0"); Logger.println(""); Logger.println("Libération lecture (%s, %s)", lecture1, lecture2); verrou.unlockRead(lecture1); verrou.unlockRead(lecture2); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0");longecriture=verrou.tryWriteLock(); Logger.println("Écriture (%s)", ecriture); Logger.println("Conversion en lecture de (%s) en (%s)", ecriture, verrou.tryConvertToReadLock(ecriture)); Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ?"1":"0");StampedLock - Conversion en lecture (console)Sélectionnez1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.00:00:00.022 [main ] Lecture optimiste (256) 00:00:00.030 [main ] Verrous lecture(0) écriture(0) 00:00:00.030 [main ] Conversion en lecture de (256) en (257) 00:00:00.031 [main ] Verrous lecture(1) écriture(0) 00:00:00.031 [main ] 00:00:00.031 [main ] Lecture (258) 00:00:00.031 [main ] Verrous lecture(2) écriture(0) 00:00:00.032 [main ] Conversion en lecture de (258) en (258) 00:00:00.032 [main ] Verrous lecture(2) écriture(0) 00:00:00.032 [main ] 00:00:00.032 [main ] Libération lecture (257, 258) 00:00:00.033 [main ] Verrous lecture(0) écriture(0) 00:00:00.033 [main ] Écriture (384) 00:00:00.033 [main ] Conversion en lecture de (384) en (513) 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
//com.developpez.lmauzaize.java.concurrence.ch04_verrous.StampedLockConversionEcriture
StampedLock verrou = new StampedLock();
long optimiste = verrou.tryOptimisticRead();
Logger.println("Lecture optimiste (%s)", optimiste);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
long ecriture = verrou.tryConvertToWriteLock(optimiste);
Logger.println("Conversion en écriture de (%s) en (%s)", optimiste, ecriture);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
Logger.println("Libération écriture (%s)", ecriture);
verrou.unlockWrite(ecriture);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
Logger.println("");
long lecture1 = verrou.tryReadLock();
Logger.println("Lecture (%s)", lecture1);
long lecture2 = verrou.tryReadLock();
Logger.println("Lecture (%s)", lecture2);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
écriture = verrou.tryConvertToWriteLock(lecture1);
Logger.println("Conversion en écriture de (%s) en (%s)", lecture1, ecriture);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
Logger.println("");
Logger.println("Libération écriture (%s)", lecture2);
verrou.unlockRead(lecture2);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
écriture = verrou.tryConvertToWriteLock(lecture1);
Logger.println("Conversion en écriture de (%s) en (%s)", lecture1, ecriture);
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
Logger.println("");
Logger.println("Conversion en écriture de (%s) en (%s)", ecriture, verrou.tryConvertToWriteLock(ecriture));
Logger.println("Verrous lecture(%s) écriture(%s)", verrou.getReadLockCount(), verrou.isWriteLocked() ? "1" : "0");
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
00:00:00.022 [main ] Lecture optimiste (256)
00:00:00.030 [main ] Verrous lecture(0) écriture(0)
00:00:00.030 [main ] Conversion en écriture de (256) en (384)
00:00:00.031 [main ] Verrous lecture(0) écriture(1)
00:00:00.031 [main ] Libération écriture (384)
00:00:00.031 [main ] Verrous lecture(0) écriture(0)
00:00:00.032 [main ]
00:00:00.032 [main ] Lecture (513)
00:00:00.032 [main ] Lecture (514)
00:00:00.032 [main ] Verrous lecture(2) écriture(0)
00:00:00.033 [main ] Conversion en écriture de (513) en (0)
00:00:00.033 [main ] Verrous lecture(2) écriture(0)
00:00:00.033 [main ]
00:00:00.034 [main ] Libération écriture (514)
00:00:00.034 [main ] Verrous lecture(1) écriture(0)
00:00:00.034 [main ] Conversion en écriture de (513) en (640)
00:00:00.034 [main ] Verrous lecture(0) écriture(1)
00:00:00.035 [main ]
00:00:00.035 [main ] Conversion en écriture de (640) en (640)
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.





