Concurrence

ReentrantLock


L'API java.util.concurrent.locks

Il existe un autre mécanisme permettant de définir des sections critiques introduit en Java 5 :

  • Une interface Lock
  • Une classe qui l'implémente : ReentrantLock
  • Quel intérêt ?
    • C'est un "vrai" objet avec des méthodes et un peu plus de flexibilité (utilisation avancée) ;
    • Plusieurs signaux différents sur un même lock (Condition).

ReentrantLock

C'est également un mécanisme de verrou (appelé aussi mutex). Un objet ReentrantLock a deux méthodes principales :

  • lock() qui permet de prendre le verrou ;
  • unlock() qui permet de rendre le verrou.

Un appel à lock() doit toujours être suivi d'un appel à unlock(). Le code entre les deux est une section critique.

Section critique et exception

En Java, beaucoup de lignes de codes sont susceptibles de lever une exception...

public class StupidLockDemo {
	private final ReentrantLock lock = new ReentrantLock();

	public void increment() {
		lock.lock();
			// do anything 
		lock.unlock();
	}
}

... par conséquent, le code ci-dessus est faux, car l'appel à unlock() pourrait ne pas être effectué.

Solution

Il faut obligatoirement utiliser un bloc try/finally pour verrouiller avec un ReentrantLock

public class StupidLockDemo {
	private final ReentrantLock lock = new ReentrantLock();

	public void increment() {
		lock.lock(); // en dehors du try :)
		try{
			// do anything
		} finally {
			lock.unlock();
		}
	}
}

Garanties de la section critique


Les garanties offertes par la section critique des ReentrantLock sont les mêmes qu'avec un bloc synchronized :

  • Atomicité (exclusion mutuelle) de la section grâce au verrou.
  • Barrière en mémoire.

À condition de les utliser correctementm bien sûr :)

Équité (fairness)

Il existe deux versions de ReentrantLock : l'une équitable (fair) et l'autre non (celle par défaut).

  • La version équitable garantit que si plusieurs threads sont en attente sur un verrou c'est celui qui attend depuis le plus longtemps qui le prend (sinon c'est un thread au hasard). Elle est donnée par ReentrantLock(true).
  • Le constructeur sans paramètre ReentranLock() donne la version non équitable. C'est équivalent à ReentrantLock(false).
  • En termes d'efficacité, la version équitable est plus lente.
  • L'équité n'a rien à voir avec la priorité que le scheduler associe au thread : ce n'est pas le thread le plus prioritaire qui gagne, mais celui qui a attendu le plus longtemps.

Des méthodes en plus avec ReentrantLock...

Un ReentrantLock possède, par exemple, les méthodes d'instance :

  • tryLock() qui permet d’acquérir un verrou (comme lock()) mais qui renvoie false au lieu de bloquer si un autre thread possède déjà le verrou ; c'est une opération atomique.

  • lockInterruptibly() throws InterruptedException qui permet de prendre un verrou (comme lock()) mais qui peut être interrompue (voir cours suivant) si le thread est bloqué (parce qu'un autre thread possède déjà le verrou).

Signaux (et Condition)

Attention, on ne peut pas utiliser lock.wait() et lock.notify() car cela suppose qu'on est dans un bloc synchronized(lock) !

On utilise un objet Condition fourni par le ReentrantLock :

private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

On peut ensuite utiliser :

  • condition.await() (à la place de wait)
  • condition.signal() (à la place de notify)
  • condition.signalAll() (à la place de notifyAll)

Ces méthodes fonctionnent exactement comme leurs équivalents ...
avec le mêmes problèmes, notamment, le risque de spurious wake-up.

Exemple

public class Holder {
	private int value;
	private boolean done;
	private final ReentrantLock lock = new ReentrantLock();
	private final Condition condition = lock.newCondition();

	private void init() {
		lock.lock();
		try {
			value = 12;
			done = true;
			condition.signal();
		} finally {
			lock.unlock();
		}
	}

	private void display() throws InterruptedException {
		lock.lock();
		try {
			while (!done) {
				condition.await();
			}
			System.out.println(value);
		} finally {
			lock.unlock();
		}
	}
}

Condition multiples

... et petit bonus : l'API des Conditions permet de créer plusieurs conditions pour un même ReentrantLock. C'est pratique pour utiliser des conditions différentes liées à un même champs.

Lorsque l'on souhaite pouvoir réveiller/endormir un thread pour deux raisons différentes, il suffit d'associer une Condition à chaque raison (voir exercice de TP).

En résumé ...

Tout ce que l'on faisait avec un block synchronized peut être fait avec un ReentrantLock.

Il ne faut jamais oublier le try ... finally !

	lock.lock(); // en dehors du try :)
	try {
		// do anything
	} finally{
		lock.unlock();
	}

On peut utiliser des Condition différentes pour accorder les paires d'appels aux méthodes await/signal.

Attention : Ne jamais faire wait et notify sur une Condition !