Apprentissage du code source du package simultané Java: file d'attente de synchronisation CLH et acquisition et libération des ressources de synchronisation

Les objectifs d'apprentissage de cet article

  • Vérifiez la structure de la file d'attente de synchronisation CLH.
  • Apprenez le processus d'acquisition et de libération de ressources exclusives.

Structure de la file d'attente CLH

Je fais partie de la série d'apprentissage Java Concurrent Package Source: AbstractQueuedSynchronizer # Synchronous Queue and Node node. J'ai présenté en gros la structure de CLH. Cet article analyse principalement les opérations associées de la file d'attente synchrone, je vais donc l'examiner ici:

AQS utilise le FIFO intégré pour synchroniser la file d'attente bidirectionnelle pour terminer la mise en file d'attente du thread d'acquisition de ressources, en interne via la tête de nœud [en fait un nœud virtuel, le vrai premier thread est dans la position de head.next] et de queue pour enregistrer les éléments de tête et de queue de la file d'attente, le type d'élément de file d'attente est Node.

 

 

  • Si le thread actuel ne parvient pas à acquérir l'état de synchronisation (verrou), AQS construira un nœud (Node) et l'ajoutera à la file d'attente de synchronisation, et bloquera le thread actuel en même temps.
  • Lorsque l'état de synchronisation est libéré, le thread dans le nœud sera réveillé pour lui faire essayer d'obtenir à nouveau l'état de synchronisation.

Ensuite, nous utiliserons AQS pour acquérir et libérer exclusivement des ressources afin d'expliquer en détail le flux de travail de la file d'attente de blocage CLH intégrée, puis regarder vers le bas.

Acquisition de ressources

public final void acquire(int arg) {
        if (!tryAcquire(arg) && // tryAcquire由子类实现,表示获取锁,如果成功,这个方法直接返回了
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果获取失败,执行
            selfInterrupt();
    }

  • tryAcquire (int) est une méthode de raccordement fournie par AQS aux sous-classes. Les sous-classes peuvent personnaliser la façon d'obtenir des ressources exclusivement. Si l'acquisition réussit, elle renvoie true et si elle échoue, elle renvoie false.
  • Si la méthode tryAcquire réussit à acquérir la ressource, elle retourne directement et la transformation qui a échoué exécutera la logique d'acquérirQueued (addWaiter (Node.EXCLUSIVE), arg)). Nous pouvons la scinder en deux étapes: addWaiter (Node.EXCLUSIVE) : Enveloppez le thread dans un nœud exclusif et ajoutez-le à la file d'attente. acquitterQueued (node, arg): Si le nœud actuel est le premier nœud en attente, c'est-à-dire head.next, essayez d'acquérir des ressources. Si la méthode retourne true, elle entrera dans la logique de selfInterrupt () et block.

Ensuite, jetons un coup d'œil aux deux méthodes addWaiter et acquitQueued.

Entrez Node addWaiter (mode Node)

Déterminez le mode exclusif ou partagé en fonction du paramètre de mode entrant, créez un nœud pour le thread actuel et rejoignez l'équipe.

// 其实就是把当前线程包装一下,设置模式,形成节点,加入队列
	private Node addWaiter(Node mode) {
        // 根据mode和thread创建节点
        Node node = new Node(Thread.currentThread(), mode);
        // 记录一下原尾节点
        Node pred = tail;
        // 尾节点不为null,队列不为空,快速尝试加入队尾。
        if (pred != null) {
            // 让node的prev指向尾节点
            node.prev = pred;
            // CAS操作设置node为新的尾节点,tail = node
            if (compareAndSetTail(pred, node)) {
                // 设置成功,让原尾节点的next指向新的node,实现双向链接
                pred.next = node;
                // 入队成功,返回
                return node;
            }
        }
        // 快速入队失败,进行不断尝试
        enq(node);
        return node;
    }

Quelques points à noter:

  • L'opération de rejoindre la file d'attente consiste en fait à regrouper le thread dans un nœud de nœud via le mode spécifié. Si le nœud de fin de la file d'attente n'est pas nul, utilisez CAS pour essayer de rejoindre rapidement la fin de la file d'attente.

Il y a deux raisons à l'échec de l'inscription rapide:

  • La file d'attente est vide, c'est-à-dire qu'elle n'a pas encore été initialisée.
  • CAS n'a pas réussi à définir le nœud de queue.
  • Après le premier échec d'entrée rapide, il passera à la logique enq (nœud) et continuera d'essayer jusqu'à ce que le réglage réussisse.

Enq de nœud de test constant (nœud de nœud final)

private Node enq(final Node node) {
        // 自旋,俗称死循环,直到设置成功为止
        for (;;) {
            // 记录原尾节点
            Node t = tail;
            // 第一种情况:队列为空,原先head和tail都为null,
            // 通过CAS设置head为哨兵节点,如果设置成功,tail也指向哨兵节点
            if (t == null) { // Must initialize
                // 初始化head节点
                if (compareAndSetHead(new Node()))
                    // tail指向head,下个线程来的时候,tail就不为null了,就走到了else分支
                    tail = head;
            // 第二种情况:CAS设置尾节点失败的情况,和addWaiter一样,只不过它在for(;;)中
            } else {
                // 入队,将新节点的prev指向tail
                node.prev = t;
                // CAS设置node为尾部节点
                if (compareAndSetTail(t, node)) {
                    //原来的tail的next指向node
                    t.next = node;
                    return t;
                }
            }
        }
    }

Le processus d'enq est le processus de paramétrage facultatif de la fin de la file d'attente. Si le paramétrage réussit, il sera renvoyé. Si le réglage échoue, essayez de le régler tout le temps. L'idée est que je peux toujours attendre le jour où le réglage réussit.

Nous pouvons également constater que la tête est initialisée paresseusement. Lorsque le premier nœud tente de mettre en file d'attente, la tête est nulle. À ce stade, new Node () est utilisé pour créer un nœud qui ne représente aucun thread, en tant que nœud principal virtuel , et Nous devons noter que son waitStatus est initialisé à 0, ce qui est instructif pour notre analyse ultérieure.

Si le CAS n'a pas réussi à provoquer des tentatives répétées, laissez-le continuer le CAS.

boolean acquisQueued (Nœud, entier)

// 这个方法如果返回true,代码将进入selfInterrupt()
	final boolean acquireQueued(final Node node, int arg) {
        // 注意默认为true
        boolean failed = true;
        try {
            // 是否中断
            boolean interrupted = false;
            // 自旋,即死循环
            for (;;) {
                // 得到node的前驱节点
                final Node p = node.predecessor();
                // 我们知道head是虚拟的头节点,p==head表示如果node为阻塞队列的第一个真实节点
                // 就执行tryAcquire逻辑,这里tryAcquire也需要由子类实现
                if (p == head && tryAcquire(arg)) {
                    // tryAcquire获取成功走到这,执行setHead出队操作 
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
                // 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 死循环退出,只有tryAcquire获取锁失败的时候failed才为true
            if (failed)
                cancelAcquire(node);
        }
    }

Dequeue void setHead (Nœud)

La file d'attente de synchronisation CLU suit le FIFO. Une fois que le thread du premier nœud a libéré l'état de synchronisation, le nœud suivant est réveillé. L'opération de retrait du nœud principal de l'équipe consiste simplement à pointer le pointeur principal vers le nœud qui sera retiré de la file d'attente.

private void setHead(Node node) {
        // head指针指向node
        head = node;
        // 释放资源
        node.thread = null;
        node.prev = null;
    }

booléen shouldParkAfterFailedAcquire (Nœud, Nœud)

/**
     * 走到这有两种情况 1.node不是第一个节点 2.tryAcquire争夺锁失败了
     * 这里就判断 如果当前线程争锁失败,是否需要挂起当前这个线程
     *
     * 这里pred是前驱节点, node就是当前节点
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 前驱节点的waitStatus
        int ws = pred.waitStatus;
        // 前驱节点为SIGNAL【-1】直接返回true,表示当前节点可以被直接挂起
        if (ws == Node.SIGNAL)
            return true;
        // ws>0 CANCEL 说明前驱节点取消了排队
        if (ws > 0) {
            // 下面这段循环其实就是跳过所有取消的节点,找到第一个正常的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 将该节点的后继指向node,建立双向连接
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             * 官方说明:走到这waitStatus只能是0或propagate,默认情况下,当有新节点入队时,waitStatus总是为0
             * 下面用CAS操作将前驱节点的waitStatus值设置为signal
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false,接着会再进入循环,此时前驱节点为signal,返回true
        return false;
    }

Il existe trois cas de waitStatus pour le nœud précurseur:

L'état d'attente ne sera pas Node.CONDITION car il est utilisé dans ConditonObject

  1. ws == - 1, qui est Node.SIGNAL, ce qui signifie que le nœud actuel du nœud peut être directement suspendu. Lorsque le thread pred libère l'état de synchronisation, le thread du nœud sera réveillé.
  2. ws> 0, qui est Node.CANCELLED, indiquant que le nœud précurseur a annulé la file d'attente [peut être dépassé, peut être interrompu], vous devez trouver le nœud précurseur qui n'a pas été annulé auparavant, et continuer la recherche jusqu'à ce qu'il soit a trouvé.
  3. ws == 0 ou ws == Node.PROPAGATE :
  4. Par défaut, lorsqu'un nouveau nœud entre dans la file d'attente, waitStatus est toujours à 0. Utilisez l'opération CAS pour définir la valeur waitStatus du nœud précurseur sur signal. La prochaine fois qu'il entre, il ira à la première branche.
  5. Lorsque le verrou est libéré, l'état ws du nœud occupant le verrou sera mis à jour à 0.

PROPAGATE indique qu'en mode partagé, le nœud prédécesseur non seulement réveillera le nœud successeur, mais peut également réveiller le successeur.

Nous pouvons constater que cette méthode ne retournera pas vrai la première fois que vous entrez. La raison en est que l'état du nœud prédécesseur est SIGNAL lorsque la condition de renvoi true est vraie et que SIGNAL n'a pas été défini pour le nœud prédécesseur la première fois. Ce n'est qu'après que le CAS a défini l'état qu'il renvoie true deuxième fois qu'il entre.

Alors, quelle est la signification de SIGNAL?

Référence ici: Programmation simultanée AQS CLH Detailed # Pourquoi AQS a-t-il besoin d'un nœud de tête virtuel. WaitStatus est abrégé en ws ici, et chaque nœud a une variable ws pour indiquer l'état du nœud. Une fois initialisé, il vaut 0. S'il est annulé, le signal est -1. Si l'état d'un nœud est signal, alors lorsque le nœud libère le verrou, il doit réveiller le nœud suivant. Par conséquent, avant que chaque nœud ne dorme, si le ws du nœud prédécesseur n'est pas mis à signal, il ne sera jamais réveillé. Par conséquent, nous trouverons que lorsque le ws du nœud de lecteur actuel ci-dessus est 0 ou se propage, utilisez l'opération cas pour définir ws pour signaler, de sorte que le nœud précédent puisse se notifier lorsque le verrou est libéré.

booléen parkAndCheckInterrupt ()

private final boolean parkAndCheckInterrupt() {
        // 挂起当前线程
        LockSupport.park(this);
        return Thread.interrupted();
    }

Une fois que la méthode shouldParkAfterFailedAcquire a renvoyé true, cette méthode sera appelée pour suspendre le thread actuel.

Le thread suspendu par la méthode LockSupport.park (this) peut être réveillé de deux manières: 1. par unpark () 2. par interruption ().

Notez que Thread.interrupted () effacera ici le bit indicateur d'interruption.

void cancelAcquire (nœud)

Lorsque le tryAcquire ci-dessus ne parvient pas à acquérir le verrou, il utilisera cette méthode.

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		// 将节点的线程置空
        node.thread = null;

        // 跳过所有的取消的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        // 这里在没有并发的情况下,preNext和node是一致的
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here. 可以直接写而不是用CAS
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 设置node节点为取消状态
        node.waitStatus = Node.CANCELLED;

        // 如果node为尾节点就CAS将pred设置为新尾节点
        if (node == tail && compareAndSetTail(node, pred)) {
            // 设置成功之后,CAS将pred的下一个节点置为空
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head && // pred不是首节点
                ((ws = pred.waitStatus) == Node.SIGNAL || // pred的ws为SIGNAL 或 可以被CAS设置为SIGNAL
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) { // pred线程非空
                // 保存node 的下一个节点
                Node next = node.next; 
                // node的下一个节点不是cancelled,就cas设置pred的下一个节点为next
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 上面的情况除外,则走到这个分支,唤醒node的下一个可唤醒节点线程
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

Libérer les ressources

version booléenne (int arg)

public final boolean release(int arg) {
        if (tryRelease(arg)) { // 子类实现tryRelease方法
            // 获得当前head
            Node h = head;
            // head不为null并且head的等待状态不为0
            if (h != null && h.waitStatus != 0)
                // 唤醒下一个可以被唤醒的线程,不一定是next哦
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

  • tryRelease (int) est une méthode de raccordement fournie par AQS aux sous-classes. Les sous-classes peuvent personnaliser la manière de libérer des ressources exclusivement. La publication réussit et renvoie true, sinon elle renvoie false.
  • La méthode unparkSuccessor (node) est utilisée pour réveiller le prochain thread qui peut être réveillé dans la file d'attente, pas nécessairement le prochain nœud, par exemple, il peut être dans un état annulé.
  • Le ws de la tête ne doit pas être égal à 0, pourquoi? Lorsqu'un nœud tente de se suspendre, il définit le nœud prédécesseur sur SIGNAL -1. Même s'il est le premier nœud à rejoindre la file d'attente, après avoir échoué à acquérir le verrou, le ws défini par le nœud virtuel sera défini sur SIGNAL , et ceci Le jugement est d'empêcher la libération répétée de plusieurs threads. Ensuite, nous pouvons également voir l'opération consistant à mettre ws à 0 lors de la libération.

void unparkSuccessor (nœud de nœud)

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        // 如果node的waitStatus<0为signal,CAS修改为0
        // 将 head 节点的 ws 改成 0,清除信号。表示,他已经释放过了。不能重复释放。
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // 唤醒后继节点,但是有可能后继节点取消了等待 即 waitStatus == 1
        Node s = node.next;
        // 如果后继节点为空或者它已经放弃锁了
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从队尾往前找,找到没有没取消的所有节点排在最前面的【直到t为null或t==node才退出循环嘛】
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 如果>0表示节点被取消了,就一直向前找呗,找到之后不会return,还会一直向前
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 如果后继节点存在且没有被取消,会走到这,直接唤醒后继节点即可
        if (s != null)
            LockSupport.unpark(s.thread);
    }

Lien d'origine: https://www.cnblogs.com/summerday152/p/14244324.html

Si vous pensez que cet article vous est utile, vous pouvez suivre mon compte officiel et répondre au mot-clé [Interview] pour obtenir une compilation des points de connaissances de base Java et un coffret cadeau d'entrevue! Il y a plus d'articles techniques de produits secs et de matériaux connexes à partager, laissez tout le monde apprendre et progresser ensemble!

 

Je suppose que tu aimes

Origine blog.csdn.net/weixin_48182198/article/details/112341438
conseillé
Classement