Maison >Java >javaDidacticiel >Comment l'architecture AQS du synchroniseur Java libère-t-elle les verrous et synchronise-t-elle les files d'attente

Comment l'architecture AQS du synchroniseur Java libère-t-elle les verrous et synchronise-t-elle les files d'attente

WBOY
WBOYavant
2023-05-11 17:16:151563parcourir

    Introduction

    AQS a trop de contenu, nous l'avons donc divisé en deux chapitres. Les étudiants qui n'ont pas lu la première moitié d'AQS peuvent regarder en arrière. La première moitié du chapitre parle de nombreux concepts de base. verrous. Attributs de base, comment obtenir des verrous, etc. Dans ce chapitre, nous parlons principalement de la façon de libérer les verrous et les files d'attente de synchronisation.

    1. Libérez le verrou

    Le moment de déclenchement de la libération du verrou est notre méthode Lock.unLock () couramment utilisée. Le but est de permettre au thread de libérer l'accès aux ressources (voir la route violette dans le schéma d'architecture global pour). le processus).

    La libération des verrous est également divisée en deux catégories, l’une est la libération des verrous exclusifs et l’autre est la libération des verrous partagés. Examinons-les séparément.

    1.1. Libération du verrou exclusif

    La libération du verrou exclusif est relativement simple. Commencez par la tête de la file d'attente et trouvez son nœud suivant. Si le nœud suivant est vide, il commencera par la fin et trouvera le nœud suivant. Il ne s'agit pas d'un nœud annulé, puis libérez le nœud. Le code source est le suivant :

    // unlock 的基础方法
    public final boolean release(int arg) {
        // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。
        if (tryRelease(arg)) {
            Node h = head;
            // 头节点不为空,并且非初始化状态
            if (h != null && h.waitStatus != 0)
                // 从头开始唤醒等待锁的节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    // 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点
    // 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待
    private void unparkSuccessor(Node node) {
        // node 节点是当前释放锁的节点,也是同步队列的头节点
        int ws = node.waitStatus;
        // 如果节点已经被取消了,把节点的状态置为初始化
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 拿出 node 节点的后面一个节点
        Node s = node.next;
        // s 为空,表示 node 的后一个节点为空
        // s.waitStatus 大于0,代表 s 节点已经被取消了
        // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 这里从尾迭代,而不是从头开始迭代是有原因的。
            // 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
            for (Node t = tail; t != null && t != node; t = t.prev)
                // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 唤醒以上代码找到的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    1.2. Libérez le verrou partagé releaseShared

    La méthode de libération du verrou partagé est releaseShared, qui est principalement divisée en deux. étapes :

    tryReleaseShared essaie de libérer le verrou partagé actuel et renvoie false s'il échoue.

    réveille les nœuds bloqués suivants du nœud actuel. Nous avons déjà vu cette méthode lorsque le thread obtient le partage. lock, il réveillera le nœud derrière lui. Le nom de la méthode est : doReleaseShared.

    Jetons un coup d'œil au code source de releaseShared :

    // 共享模式下,释放当前线程的共享锁
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
            doReleaseShared();
            return true;
        }
        return false;
    }

    2. Méthodes importantes de file d'attente conditionnelle

    Avant d'examiner les méthodes de file d'attente conditionnelle, nous devons d'abord comprendre pourquoi il existe une file d'attente de synchronisation et pourquoi une file d'attente conditionnelle est disponible. nécessaire?

    Principalement parce que tous les scénarios ne peuvent pas être gérés par une file d'attente synchronisée. Lorsque vous rencontrez un scénario dans lequel verrouillage + file d'attente est combiné, Lock + Condition est requis. Utilisez d'abord Lock pour déterminer quels threads peuvent obtenir le verrou et quels threads vous devez mettre en file d'attente. et bloquer dans la file d'attente de synchronisation ; lorsque plusieurs threads qui ont obtenu le verrou rencontrent la file d'attente pleine ou vide, vous pouvez utiliser Condition pour gérer ces threads, permettant à ces threads de se bloquer et d'attendre, puis d'être réveillés normalement au moment approprié.

    Les scénarios dans lesquels la file d'attente de synchronisation + la file d'attente conditionnelle sont utilisées ensemble sont les plus couramment utilisés dans les scénarios de verrouillage + file d'attente.

    La file d'attente conditionnelle est donc également un élément indispensable.

    Jetons ensuite un coup d'œil à certaines des méthodes les plus importantes de la file d'attente de conditions. Les méthodes suivantes se trouvent toutes dans la classe interne ConditionObject.

    2.1. Entrez dans la file d'attente et attendez la méthode wait

    // 线程入条件队列
    public final void await() throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 加入到条件队列的队尾
        Node node = addConditionWaiter();
        // 标记位置 A
        // 加入条件队列后,会释放 lock 时申请的资源,唤醒同步队列队列头的节点
        // 自己马上就要阻塞了,必须马上释放之前 lock 的资源,不然自己不被唤醒的话,别的线程永远得不到该共享资源了
        int savedState = fullyRelease(node);
        int interruptMode = 0;
        // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
        // 目前想到的只有两种可能:
        // 1:node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
        // 2:线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去
        while (!isOnSyncQueue(node)) {
            // this = AbstractQueuedSynchronizer$ConditionObject
            // 阻塞在条件队列上
            LockSupport.park(this);
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }
        // 标记位置 B
        // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了
        // 所以这里节点苏醒了,直接尝试 acquireQueued
        if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
            interruptMode = REINTERRUPT;
        if (node.nextWaiter != null) // clean up if cancelled
            // 如果状态不是CONDITION,就会自动删除
            unlinkCancelledWaiters();
        if (interruptMode != 0)
            reportInterruptAfterWait(interruptMode);
    }

    await Il y a plusieurs points qui nécessitent une attention particulière :

    Le code ci-dessus marque la position A. Avant que le nœud ne soit prêt à entrer dans la file d'attente conditionnelle, il doit d'abord être libéré. le verrou actuellement détenu, sinon il entrera tout seul. La file d'attente des conditions est dans la file d'attente des conditions et les autres threads ne peuvent pas obtenir le verrou ci-dessus. Méthode signalAll.À ce stade, le nœud a été transféré avec succès dans la file d'attente de synchronisation (dans l'ensemble, le processus bleu dans le diagramme d'architecture), de sorte que la méthode acquireQueued peut être exécutée directement dans le code source pour nommer Node dans la condition ; file d'attente, nous voyons donc Waiter dans la file d'attente des conditions, qui est en fait Node. Il existe deux méthodes importantes dans la méthode

    await : addConditionWaiter et unlinkCancelledWaiters. Examinons-les une par une.

    2.1.1, la méthode addConditionWaiter

    addConditionWaiter place principalement les nœuds dans la file d'attente des conditions. Le code source de la méthode est le suivant :

    // 增加新的 waiter 到队列中,返回新添加的 waiter
    // 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点
    // 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上
    private Node addConditionWaiter() {
        Node t = lastWaiter;
        // If lastWaiter is cancelled, clean out.
        // 如果尾部的 waiter 不是 CONDITION 状态了,删除
        if (t != null && t.waitStatus != Node.CONDITION) {
            unlinkCancelledWaiters();
            t = lastWaiter;
        }
        // 新建条件队列 node
        Node node = new Node(Thread.currentThread(), Node.CONDITION);
        // 队列是空的,直接放到队列头
        if (t == null)
            firstWaiter = node;
        // 队列不为空,直接到队列尾部
        else
            t.nextWaiter = node;
        lastWaiter = node;
        return node;
    }

    Le processus global est relativement simple, qui consiste à ajouter à la fin de la file d'attente. une méthode importante appelée unlinkCancelledWaiters. Cette méthode Tous les nœuds de la file d'attente de conditions dont le statut n'est pas CONDITION seront supprimés. Jetons un coup d'œil au code source de la méthode unlinkCancelledWaiters, comme suit :

    2.1.2, unlinkCancelledWaiters
    // 会检查尾部的 waiter 是不是已经不是CONDITION状态了
    // 如果不是,删除这些 waiter
    private void unlinkCancelledWaiters() {
        Node t = firstWaiter;
        // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
        Node trail = null;
        while (t != null) {
            Node next = t.nextWaiter;
            // 当前node的状态不是CONDITION,删除自己
            if (t.waitStatus != Node.CONDITION) {
                //删除当前node
                t.nextWaiter = null;
                // 如果 trail 是空的,咱们循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
                // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
                if (trail == null)
                    firstWaiter = next;
                // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
                else
                    trail.nextWaiter = next;
                // 遍历结束,最后一次找到的CONDITION节点就是尾节点
                if (next == null)
                    lastWaiter = trail;
            }
            // 状态是 CONDITION 的 Node
            else
                trail = t;
            // 继续循环,循环顺序从头到尾
            t = next;
        }
    }

    In. Afin de faciliter la compréhension de cette méthode par tous, j'ai dessiné un schéma explicatif, comme suit :

    Comment larchitecture AQS du synchroniseur Java libère-t-elle les verrous et synchronise-t-elle les files dattente

    2.2. Signal de réveil unique

    méthode du signal signifie réveil Par exemple, la file d'attente était pleine avant, certains fils de discussion. ont été bloqués dans la file d'attente conditionnelle en raison d'une opération de prise, et tout à coup, les éléments de la file d'attente ont été consommés par le thread A. Le thread A appellera la méthode signal pour réveiller le thread précédemment bloqué. Il se réveillera à partir du nœud principal de la condition. file d'attente (voir la partie bleue dans le schéma d'architecture globale du processus). Le code source est le suivant :

    // 唤醒阻塞在条件队列中的节点
    public final void signal() {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        // 从头节点开始唤醒
        Node first = firstWaiter;
        if (first != null)
            // doSignal 方法会把条件队列中的节点转移到同步队列中去
            doSignal(first);
    }
    // 把条件队列头节点转移到同步队列去
    private void doSignal(Node first) {
        do {
            // nextWaiter为空,说明到队尾了
            if ( (firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了
            // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂
            first.nextWaiter = null;
            // transferForSignal 方法会把节点转移到同步队列中去
            // 通过 while 保证 transferForSignal 能成功
            // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)
            // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了
        } while (!transferForSignal(first) &&
                 (first = firstWaiter) != null);
    }

    Jetons un coup d'œil à la méthode la plus critique : transferForSignal.

    // 返回 true 表示转移成功, false 失败
    // 大概思路:
    // 1. node 追加到同步队列的队尾
    // 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒
    // 可以看出来 node 的状态此时是 0 了
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点
        // 看命名是 p,实际是 pre 单词的缩写
        Node p = enq(node);
        int ws = p.waitStatus;
        // 状态修改成 SIGNAL,如果成功直接返回
        // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
            LockSupport.unpark(node.thread);
        return true;
    }

    Après avoir lu l'intégralité du code source, nous pouvons voir que le réveil des nœuds dans la file d'attente de conditions transfère en fait les nœuds de la file d'attente de conditions vers la file d'attente de synchronisation et définit le statut de son nœud précédent sur SIGNAL.

    2.3. Réveiller tous les signauxAll

    signalAll est utilisé pour réveiller tous les nœuds de la file d'attente des conditions. Le code source est le suivant :

        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 拿到头节点
            Node first = firstWaiter;
            if (first != null)
                // 从头节点开始唤醒条件队列中所有的节点
                doSignalAll(first);
        }
        // 把条件队列所有节点依次转移到同步队列去
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                // 拿出条件队列队列头节点的下一个节点
                Node next = first.nextWaiter;
                // 把头节点从条件队列中删除
                first.nextWaiter = null;
                // 头节点转移到同步队列中去
                transferForSignal(first);
                // 开始循环头节点的下一个节点
                first = next;
            } while (first != null);
        }

    Comme le montre le code source, son essence est que les appels de la boucle for. la méthode transferForSignal pour faire défiler les nœuds dans la file d’attente de conditions Transférer vers la file d’attente de synchronisation.

    3. Résumé

    Le code source d'AQS est enfin terminé. Le comprenez-vous ? Vous pouvez rappeler silencieusement le diagramme d'architecture AQS et voir si vous pouvez le comprendre maintenant.

    Comment larchitecture AQS du synchroniseur Java libère-t-elle les verrous et synchronise-t-elle les files dattente

    Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

    Déclaration:
    Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer