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.StampedLockConversionOptimiste
StampedLock verrou=
new
StampedLock
(
);long
optimiste=
verrou.tryOptimisticRead
(
); Logger.println
(
"Lecture optimiste (%s)"
, optimiste); Logger.println
(
"Conversion en lecture optimiste de (%s) en (%s)"
, optimiste, verrou.tryConvertToOptimisticRead
(
optimiste)); Logger.println
(
""
);long
lecture=
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
(
""
);long
ecriture=
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.StampedLockConversionLecture
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
lecture1=
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
(
""
);long
lecture2=
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"
);long
ecriture=
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.