Fondements de l'AQS
L'AbstractQueuedSynchronizer (AQS) est un cadre de synchronisation concurrent en Java qui sert de base pour implémenter des mécanismes de verrouillage et de coordination tels que les verrous réentrants, les sémaphores et les compteurs à rebours. Son architecture repose sur une file d'attente de threads gérée via une structure de données modifiée de type CLH, combinée à des opérations atomiques sans verrou pour la gestion de l'état.
Principes d'implémentation
Structure de la file d'attente
AQS utilise une liste doublement chaînée pour organiser les threads en attente, contrairement à la file CLH originale qui est une liste chaînée simple. Cette conception permet un accès direct aux prédécesseurs et successeurs, facilitant la gestion des annulations et des réveils. Chaque nœud de la file contient des informations sur le thread associé et un état d'attente, représenté par des constantes comme SIGNAL (indiquant que le successeur doit être réveillé) ou CANCELLED (marquant un thread annulé).
Voici un exemple simplifié d'un nœud de file d'attente :
public class QueueElement {
private QueueElement prevNode;
private QueueElement nextNode;
private Thread associatedThread;
private volatile int nodeStatus; // Valeurs : SIGNAL = -1, CANCELLED = 1, etc.
}
Les avantages incluent une réduction du trafic de cohérence de cache CPU, car chaque thread surveille principalement l'état de son prédécesseur, et une gestion efficace des suppressions de nœuds en temps constant.
Gestion de l'état et opérations CAS
La variable d'état interne state représente la disponibilité de la ressource partagée. En mode exclusif, elle indique souvent le nombre de réetnrances d'un verrou ; en mode partagé, elle représente le nombre de ressources disponibles. Les modifications de cet état sont effectuées via des opérations Compare-And-Swap (CAS) pour assurer l'atomicité sans verrou.
protected final boolean casUpdateState(int expectedVal, int newVal) {
return unsafe.compareAndSwapInt(this, stateOffset, expectedVal, newVal);
}
Cette approche évite les problèmes de contention élevée en combinant le CAS avec une file d'attente pour ordonner les threads, plutôt que d'utiliser des verrous traditionnels.
Modes de synchronisation
AQS supporte deux modes principaux : exclusif, où une seule thread peut accéder à la ressource à la fois (par exemple, ReentrantLock), et partagé, où plusieurs threads peuvent accéder simultanément (par exemple, Semaphore). Les sous-classes doivent implémenter des méthodes spécifiques pour chaque mode.
| Caractéristique | Mode exclusif | Mode partagé |
|---|---|---|
| Occupation des ressources | Unique à la fois | Multiple simultanée |
| Exemples d'implémentation | ReentrantLock | Semaphore, CountDownLatch |
| Sémantique de l'état | Nombre de réentrances | Nombre de ressources disponibles |
| Stratégie de réveil | Réveil d'un seul successeur | Réveil potentiel de plusieurs successeurs |
Méthodes fondamentales
Acquisition et libération en mode exclusif
Pour acquérir une ressource, la méthode acquire tente d'obtenir la ressource via tryAcquire. Si elle échoue, le thread est ajouté à la file d'attente et mis en sommeil jusqu'à ce qu'il soit réveillé.
public final void obtainResource(int delta) {
if (!tryAcquire(delta) &&
parkAfterEnqueue(addToQueue(WaitingNode.EXCLUSIVE_MODE), delta))
handleThreadInterrupt();
}
Dans cette méthode, tryAcquire est une méthode abstraite à implémenter par les sous-classes. addToQueue crée un nouveau nœud et l'insère dans la file, avec une tentative rapide de CAS pour l'ajout en queue.
La libération de la ressource est effectuée via release, qui appelle tryRelease et, si nécessaire, réveille un thread en attente.
public final boolean freeResource(int delta) {
if (tryRelease(delta)) {
QueueElement headNode = queueHead;
if (headNode != null && headNode.nodeStatus != 0)
wakeUpSuccessor(headNode);
return true;
}
return false;
}
La méthode wakeUpSuccessor parcourt la file pour trouver un thread valide à réveiller, en privilégiant l'accès direct au successeur ou en remontant depuis la queue si nécessaire.
Opérations en mode partagé
Pour le mode partagé, des méthodes comme acquireShared et releaseShared sont utilisées, avec une propagation des signaux de réveil pour permettre à plusieurs threads d'accéder à la ressource.
public final void obtainSharedResource(int delta) {
if (tryAcquireShared(delta) < 0) {
enqueueForSharing(delta);
}
}
La méthode enqueueForSharing suit une logique similaire à celle du mode exclusif, mais avec une gestion spéciale pour le réveil en cascade via l'état PROPAGATE.
Considérations pour les entretiens techniques
Conception de la file d'attente
L'utilisation d'une liste doublement chaînée dans AQS permet une gestion efficace des threads annulés, car la suppression peut être effectuée en temps constant sans parcours complet de la file. Cela est crucial pour les scénarios d'interruption ou de timeout, où les threads doivent être retirés proprement de la file d'attente.
Réentrance et gestion de l'état
La réentrance est implémentée en utilisant la variable d'état pour compter le nombre d'acquisitions successives par le même thread. Lors de la libération, le compteur est décrémenté, et la ressource n'est réellement libérée que lorsque le compteur atteint zéro, ce qui garantit qu'un thread peut acquérir plusieurs fois le même verrou sans blocage.
Mécanisme de réveil optimisé
Le réveil des threads est effectué de manière précise via LockSupport.unpark, ciblant uniquement le prochain thread valide dans la file. Cette approche minimise les changements de contexte par rapport aux méthodes comme notifyAll, et supporte un mécanisme de "permission" où un appel à unpark antérieur à park n'entraîne pas de blocage.
Optimisations de performance
AQS intègre des techniques telles que l'autospinning avant le blocage pour réduire les appels à park, la gestion des nœuds annulés pour nettoyer la file, et l'optimisation des verrous non équitables où les nouveaux threads peuvent tenter une acquisition immédiate via CAS, contournant la file d'attente. Ces conceptions réduisent la contention et améliorent le débit dans les scénarios concurrents à haute charge.