ReentrantLock : une analyse technique approfondie

ReentrantLock

ReentrantLock permet d'acquérir un verrou via la méthode lock() et de le libérer avec unlock(). Il est impératif d'utiliser la même instance de ReentrantLock pour le verrouillage. Ce mécanisme est basé sur le framework de synchronisation Java AbstractQueuedSynchronizer (AQS).

AQS maintient un état de synchronisation à l'aide d'une variable volatile de type entier nommée state. Examinons donc les fondements de l'AQS.

Fondements de l'AQS

Variables, constantes et classes internes de l'AQS

Les éléments clés comprennent head, tail et une classe interne Node, qui forment enesmble une file d'attente à double chaînage pour stocker les threads.

Classe interne Node

Elle contient des champs prev, next et thread, ainsi que des indicateurs d'état comme CANCELLED, CONDITION, PROPAGATE et SIGNAL. Cette classe est finale, mais ses champs prev, next et thread sont modifiables.

Structure de ReentrantLock

ReentrantLock inclut trois classes internes : FairSync pour le verrou équitable, NonfairSync pour le verrou non équitable, et Sync pour le verrou de base.

FairSync : Verrou équitable

FairSync hérite de Sync et implémente la logique du verrou équitable.

Méthode lock()

Elle invoque la méthode acquire() d'AQS avec un argument de 1, car Sync étend AQS.

Méthode acquire() d'AQS

Cette méthode tente d'acquérir le verrou et, si échec, ajoute le thread à la file d'attente.

Méthode tryAcquire()

Voici une implémentation réécrite de la version équitabel :


protected final boolean obtenirVerrou(int acquisition) {
    final Thread threadCourant = Thread.currentThread();
    int etat = obtenirEtat();
    if (etat == 0) {
        if (!aDesPredecesseursEnFile() && compareAndSetEtat(0, acquisition)) {
            definirProprietaireExclusif(threadCourant);
            return true;
        }
    } else if (threadCourant == obtenirProprietaireExclusif()) {
        int prochainEtat = etat + acquisition;
        if (prochainEtat < 0)
            throw new Error("Limite de verrouillage dépassée");
        definirEtat(prochainEtat);
        return true;
    }
    return false;
}

Explication : On obtient d'abord le thread actuel et l'état du verrou (0 signifie libre). Pour un verrou équitable, on vérifie s'il y a des prédécesseurs en file avec aDesPredecesseursEnFile(). Si non, on utilise CAS pour acquérir le verrou. Si le verrou est déjà détenu par le thread courant, on incrémente l'état pour permettre le réentrance.

Méthode obtenirEtat()

Retourne la variable state de l'AQS.

Méthode aDesPredecesseursEnFile()

Cette méthode vérifie si le thread doit attendre en file. Elle compare la tête et la queue de la file, et si la tête n'est pas nulle, elle vérifie si le prochain nœud est le thread courant.

Méthode definirProprietaireExclusif()

Définit le propriétaire exclusif du verrou sur le thread courant.

Résumé de tryAcquire()

Le thread tente d'acquérir le verrou s'il est libre et s'il n'a pas besoin de faire la queue. Si le verrou est détenu par lui-même, il réentre ; sinon, il échoue.

Retour à la méthode acquire() d'AQS

Si une mise en file est nécessaire, ajouterEnFile() est appelé pour insérer le nœud dans la file.

Méthode ajouterEnFile()

Utilise l'insertion en queue avec CAS pour garantir la cohérence.

Méthode acquisitionEnFile()

Cette méthode gère l'attente active (spin) pour les threads en file :


final boolean acquisitionEnFile(final Node noeud, int arg) {
    boolean echoue = true;
    try {
        boolean interrompu = false;
        for (;;) {
            final Node pred = noeud.predecesseur();
            if (pred == tete && obtenirVerrou(arg)) {
                definirTete(noeud);
                pred.suivant = null;
                echoue = false;
                return interrompu;
            }
            if (doitBloquerApresEchec(pred, noeud) && parcoursEtVerificationInterruption())
                interrompu = true;
        }
    } finally {
        if (echoue)
            annulerAcquisition(noeud);
    }
}

Explication : Le thread tourne en boucle pour acquérir le verrou. Si son prédécesseur est la tête et qu'il obtient le verrou, il devient la tête. Sinon, il vérifie s'il doit être bloqué.

Méthode definirTete()

Définit le nœud comme nouvelle tête et efface son thread pour maintenir l'invariant que les threads actifs ne sont pas dans la file.

Méthode doitBloquerApresEchec()

Vérifie l'état du prédécesseur pour déterminer si le thread doit être parké :


private static boolean doitBloquerApresEchec(Node pred, Node noeud) {
    int etatAttente = pred.etatAttente;
    if (etatAttente == Node.SIGNAL)
        return true;
    if (etatAttente > 0) {
        do {
            noeud.precedent = pred = pred.precedent;
        } while (pred.etatAttente > 0);
        pred.suivant = noeud;
    } else {
        compareAndSetEtatAttente(pred, etatAttente, Node.SIGNAL);
    }
    return false;
}

L'état SIGNAL (-1) indique que le thread doit être bloqué. La méthode ajuste l'état des prédécesseurs pour signaler l'attente.

Méthode parcoursEtVerificationInterruption()

Bloque le thread via LockSupport.parcours() et vérifie les interruptions.

Méthode annulerAcquisition()

Annule l'acquisition du verrou en cas d'erreur, nettoyant la file d'attente.

Verrou non équitable

Le verrou non équitable omet la vérification de aDesPredecesseursEnFile(), permettant ainsi aux threads de tenter d'acquérir le verrou immédiatement sans respect de l'ordre.

Étiquettes: Java ReentrantLock AQS Concurrence verrouillage

Publié le 11 juin à 17h44