Les files d'attente conditionnelles jouent un rôle crucial dans le framework AQS (AbstractQueuedSynchronizer) de Java, permettant une gestion avancée des threads concurrents. Contrairement aux mécanismes de base comme les verrous exclusifs ou partagés, elles offrent des fonctionnalités spécifiques pour synchroniser des threads en attente de conditions particulières.
Dans JDK, les files d'attente conditionnelles sont implémentées via les méthodes natives wait(), notify() et notifyAll() de la classe Object. Pour utiliser wait(), un thread doit d'abord acquérir un verrou synchronisé sur l'objet. Par exemple :
synchronized (mutex) {
// Effectuer des opérations préliminaires
mutex.wait(); // Libérer le verrou et entrer dans la file d'attente
// Reprendre après réveil
}
Lorsqu'un thread appelle wait(), il libère le verrou et est placé dans une file d'attente interne. Les autres threads peuvent le réveiller en appelant notify() (pour un seul thread) ou notifyAll() (pour tous les threads). Cependant, après le réveil, les threads doivent réacquérir le verrou avant de continuer, ce qui garantit l'exécution séquentielle des sections critiques.
Le framework AQS propose une alternative plus flexible avec la classe interne ConditionObject, accessible via Lock.newCondition(). Les méthodes équivalentes sont await(), signal() et signalAll(). Exemple d'utilisation :
ReentrantLock synchronizer = new ReentrantLock();
Condition cond = synchronizer.newCondition();
synchronizer.lock();
try {
cond.await(); // Suspendre le thread
} catch (InterruptedException e) {
// Gérer l'interruption
} finally {
synchronizer.unlock();
}
Structurellement, AQS maintient deux types de files d'attente : une file de blocage (FIFO) pour les threads attendant le verrou, et une file conditionnelle (liste chaînée) pour ceux en attente de conditions. Chaque nœud dans la file conditionnelle a un état waitStatus de -2. Lorsqu'un signal est envoyé, les nœuds sont transférés de la file conditionnelle à la file de blocage pour la réacquisition du verrou.
Pour évaluer les performances, des benchmarks comparant JDK et AQS peuvent être réalisés. Par exemple, en lançant 10 threads qui attendent et sont réveillés par notifyAll() ou signalAll(), AQS peut montrer des temps légèrement plus élevés due à sa gestion plus complexe. Voici un extrait de test pour AQS :
public class AQSConditionTest {
private final ReentrantLock verrou = new ReentrantLock();
private final Condition cond = verrou.newCondition();
public void runScenario() throws InterruptedException {
List<Thread> threads = new ArrayList<>();
AtomicInteger lockCount = new AtomicInteger(0);
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
verrou.lock();
lockCount.incrementAndGet();
cond.await();
verrou.unlock();
});
t.start();
threads.add(t);
}
// Attendre que tous les threads aient acquis le verrou
while (lockCount.get() < 10) {
Thread.yield();
}
verrou.lock();
cond.signalAll();
verrou.unlock();
for (Thread t : threads) {
t.join();
}
}
}
Les différences clés incluent la gestion des états des threads : JDK utilise BLOCKED pour l'acquisition de verrou et WAITING pour wait(), tandis qu'AQS utilise WAITING pour les deux opérations. Pour éviter les blocages, il peut être nécessaire d'utiliser des compteurs atomiques ou des verrous équitables.
L'avantage principal d'AQS réside dans ses API supplémentaires pour interroger l'état des files d'attente. Par exemple, hasWaiters(), getWaitQueueLength() et getWaitingThreads() permettent un contrôle plus fin. De plus, AQS offre des fonctionnalités telles que la réponse aux enterruptions, les tentatives de verrouillage (tryLock()), et la configuration de verrous équitables ou non-équitables.