synchronized
pour synchroniser des blocs d'instruction.
Les classes et interfaces que nous allons présenter dans cette partie ne sont pas toutes des verrous à proprement parler. Cela dit, elles permettent toutes de contrôler la concurrence d'accès à des blocs, c'est la raison pour laquelle nous les avons regroupées ici.
Lock
et ses implémentations sont là pour régler ce problème.
Une instance de
Lock
est là pour contrôler l'accès à une variable partagée par plusieurs
threads
à la fois. Une variable contrôlée par une instance de
Lock
ne peut être modifiée que par un seul
thread
à la fois, celui qui possède ce verrou.
Notons immédiatement que la gestion de la mémoire obéit aux même règles lorsque l'on entre dans un bloc gardé par une instance de
Lock
et que l'on en sort, que lorsque l'on entre et l'on sort d'un bloc synchronisé normal. Toutes les variables modifiées par le
thread
qui rend le verrou sont automatiquement et immédiatement vues par l'ensemble des autres
threads
qui ont une vue sur ces variables.
Lock
expose les méthodes suivantes.
lock()
: tente d'acquérir ce verrou. Cette méthode rend la main quand le verrou a été acquis. Donc si un autre
thread
possède ce verrou, le
thread
courant attendra que cet autre
thread
le rende.
lockInterruptibility()
: tente d'acquérir ce verrou, sauf si le
thread
courant est dans l'état
INTERRUPTED
.
tryLock()
et
tryLock(long time, TimeUnit unit)
: tente d'acquérir ce verrou. S'il est immédiatement disponible, alors ces deux méthodes retournent
true
. S'il ne l'est pas, la première retourne immédiatement
false
, la seconde attend le temps indiqué en paramètre qu'il se libère.
unlock()
: libère ce verrou. Il est indispensable que cette méthode soit appelée dans tous les cas. Il faut donc appeler cette méthode dans une clause
finally
.
Lock
. Elle n'expose que deux méthodes :
readLock()
: retourne un verrou en lecture.
writeLock()
: retourne un verrou en écriture.
Lock
. Leur sémantique est un peu particulière.
Une instance de
ReadWriteLock
peut fournir deux types de verrous : un verrou de lecture et un verrou d'écriture. Cette instance peut fournir autant de verrous de lecture que l'on veut, tant qu'aucun verrou d'écriture n'a été demandé. En revanche, un verrou d'écriture est exclusif : il ne peut y avoir qu'un seul
thread
qui puisse en demander un.
Ce type de verrou est efficace dans les cas, nombreux, dans lesquels les écritures sont rares par rapport aux lectures. Les lectures peuvent se faire en concurrence d'accès sans problème, seules les écritures doivent être synchronisées.
Exemple 85. Cas de deux blocs synchronisés sur le même objet
public class Marin { public synchronized vogue() { // je vogue ! } } public class Capitaine extends Marin { public synchronized vogue() { // je vogue aussi ! super.vogue() ; } }
vogue()
sur une instance de
Capitaine
appellera également la méthode
vogue()
de la classe
Marin
. Ces deux méthodes sont synchronisées sur le même objet,
this
. Que se passe-t-il alors ? Un
thread
possède un verrou, tente d'entrer dans un bloc synchronisé dont le verrou n'est pas disponible, mais il se trouve que c'est ce
thread
qui le possède.
La synchronisation en Java est dite
réentrante
, cela signifie que dans ce cas, le
thread
qui possède le bon verrou, peut entrer dans le nouveau bloc synchronisé.
Dans le cas des instances de
Lock
, ce point n'est pas spécifié, il est donc de la responsabilité de la classe d'implémentation de décider si elle est réentrante ou pas. Les implémentations fournies avec l'API standard le sont.
RentrantLock
est une implémentation de
Lock
, de caractère réentrant, comme son nom peut le laisser supposer.
Elle expose quelques méthodes supplémentaires par rapport à l'interface
Lock
, très utile.
getHoldCount()
: retourne le nombre de fois que la méthode
lock()
de ce verrou a été appelée, sans que sa méthode
unlock()
ne l'a été. Rappelons que pour tout appel à
lock()
, il doit y avoir un appel à
unlock()
, et que ce verrou ne sera ouvert que s'il y a eu autant d'appels pour ces deux méthodes.
getQueueLength()
: retourne le nombre de
threads
en attente sur ce verrou.
hasQueuedThreads()
et
hasQueueThread(Thread t)
: retourne
true
si des
threads
sont en attente sur ce verrou, ou si le
thread
passé en paramètre est dans cette file d'attente.
isHeldByCurrentThread()
: retourne
true
si le
thread
courant est en attente sur ce verrou.
isLocked()
: retourne
true
si ce verrou a été pris par un
thread
.
Exemple 86. Utilisation d'un
RentrantLock
public class Marin { // création d'une instance de ReentrantLock private Lock lock = new ReentrantLock() ; public void vogue() { // acquisition du verrou // cette méthode ne renvoie la main que lorsque // le verrou est disponible lock.lock() ; try { // traitement } catch (...) { // le bloc catch est facultatif } finally { // en revanche, la clause finally est obligatoire ! lock.unlock() ; } } }
ReadWriteRentrantLock
implémente la sémantique de l'interface
ReadWriteLock
. Elle expose quelques méthodes supplémentaires, analogues à celle de
ReentrantLock
, qui permettent d'examiner les
threads
en attente de verrous.
Dans la mesure où les verrous de lecture sont différents du verrou d'écriture, le mécanisme de réentrance est légèrement différent. Les verrous de lecture sont réentrant sans problème, ce qui est naturel.
Un
thread
qui possède un verrou de lecture ne peut pas obtenir de verrou d'écriture sur le même objet instance de
ReadWriteRentrantLock
. Il doit tout d'abord libérer son verrou de lecture.
En revanche, un
thread
qui possède un verrou d'écriture peut acquérir un verrou de lecture sur le même objet, puis libérer son verrou d'écriture.