Aprendizagem do código-fonte do pacote simultâneo Java: fila de sincronização CLH e aquisição e liberação de recursos de sincronização

Os objetivos de aprendizagem deste artigo

  • Revise a estrutura da fila de sincronização CLH.
  • Aprenda o processo de aquisição e liberação de recursos exclusivos.

Estrutura da fila CLH

Estou na série de aprendizado Java Concurrent Package Source: AbstractQueuedSynchronizer # Synchronous Queue and Node node. Apresentei aproximadamente a estrutura do CLH. Este artigo analisa principalmente as operações relacionadas da fila síncrona, por isso vou revisá-lo aqui:

AQS usa o FIFO embutido para sincronizar a fila bidirecional para completar o enfileiramento do thread de aquisição de recursos, internamente por meio do nó principal [na verdade, um nó virtual, o primeiro thread real está na posição de head.next] e tail para registrar os elementos iniciais e finais da fila, o tipo de elemento da fila é Nó.

 

 

  • Se o encadeamento atual falhar em adquirir o estado de sincronização (bloqueio), o AQS construirá um nó (Nó) e o adicionará à fila de sincronização e bloqueará o encadeamento atual ao mesmo tempo.
  • Quando o estado de sincronização for liberado, o thread no nó será ativado para fazer com que tente obter o estado de sincronização novamente.

A seguir, usaremos o AQS para adquirir e liberar recursos exclusivamente para explicar o fluxo de trabalho da fila de bloqueio CLH embutida em detalhes e, em seguida, olhar para baixo.

Aquisição de recursos

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

  • tryAcquire (int) é um método de gancho fornecido pelo AQS para subclasses. As subclasses podem personalizar a maneira de obter recursos exclusivamente. Se a aquisição for bem-sucedida, ela retornará verdadeiro e, se falhar, retornará falso.
  • Se o método tryAcquire obtiver sucesso na aquisição do recurso, ele retornará diretamente, e a transformação com falha executará a lógica de purchaseQueued (addWaiter (Node.EXCLUSIVE), arg)). Podemos dividi-lo em duas etapas: addWaiter (Node.EXCLUSIVE) : Envolva o encadeamento em um nó exclusivo e adicione-o à fila. adquiriQueued (nó, arg): Se o nó atual é o primeiro nó em espera, ou seja, head.next, tente adquirir recursos. Se o método retornar verdadeiro, ele entrará na lógica de selfInterrupt () e bloqueará.

A seguir, vamos dar uma olhada nos dois métodos addWaiter e absorbQueued.

Entrar no Node addWaiter (modo Node)

Determine o modo exclusivo ou compartilhado de acordo com o parâmetro do modo de entrada, crie um nó para o encadeamento atual e junte-se à equipe.

// 其实就是把当前线程包装一下,设置模式,形成节点,加入队列
	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;
    }

Alguns pontos a serem observados:

  • A operação de ingressar na fila é, na verdade, empacotar o encadeamento em um nó Nó por meio do modo especificado. Se o nó final da fila não for nulo, use o CAS para tentar ingressar rapidamente no fim da fila.

Existem duas razões para o fracasso da inscrição rápida:

  • A fila está vazia, ou seja, ainda não foi inicializada.
  • O CAS falhou ao definir o nó final.
  • Após a primeira falha de entrada rápida, ele irá para a lógica enq (nó) e continuará tentando até que a configuração seja bem-sucedida.

Enq do nó de teste constante (nó do nó 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;
                }
            }
        }
    }

O processo de enq é o processo de configuração opcional do fim da fila. Se a configuração for bem-sucedida, ele retornará. Se a configuração falhar, tente configurá-la o tempo todo. A ideia é que eu sempre posso esperar o dia em que a configuração será bem-sucedida.

Também podemos descobrir que a cabeça é inicializada lentamente. Quando o primeiro nó tenta enfileirar, a cabeça é nula. Nesse momento, new Node () é usado para criar um nó que não representa nenhum thread, como o nó principal virtual , e Precisamos observar que seu waitStatus é inicializado em 0, o que é instrutivo para nossa análise subsequente.

Se o CAS falhou em causar tentativas repetidas, deixe-o continuar o CAS.

boolean purchaseQueued (Node, int)

// 这个方法如果返回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);
        }
    }

Desenfileirar void setHead (Node)

A fila de sincronização CLU segue o FIFO. Depois que o thread do primeiro nó libera o estado de sincronização, o próximo nó é ativado. A operação de retirar da fila o nó principal da equipe é, na verdade, apenas apontar o ponteiro principal para o nó que será retirado da fila.

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

boolean shouldParkAfterFailedAcquire (Node, Node)

/**
     * 走到这有两种情况 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;
    }

Existem três casos de waitStatus para o nó precursor:

O estado de espera não será Node.CONDITION porque é usado em ConditonObject

  1. ws == - 1, que é Node.SIGNAL, o que significa que o nó do nó atual pode ser suspenso diretamente.Quando o encadeamento pred libera o estado de sincronização, o encadeamento do nó será ativado.
  2. ws> 0, que é Node.CANCELLED, indicando que o nó precursor cancelou a fila [pode ter expirado, pode ser interrompido], você precisa encontrar o nó precursor que não foi cancelado antes e continuar pesquisando até que seja encontrado.
  3. ws == 0 ou ws == Node.PROPAGATE :
  4. Por padrão, quando um novo nó entra na fila, waitStatus é sempre 0. Use a operação CAS para definir o valor waitStatus do nó precursor para sinalizar.A próxima vez que ele entrar, irá para o primeiro ramo.
  5. Quando o bloqueio for liberado, o status ws do nó que ocupa o bloqueio será atualizado para 0.

PROPAGATE indica que no modo compartilhado, o nó predecessor não apenas despertará o nó sucessor, mas também poderá despertar o sucessor.

Podemos descobrir que esse método não retornará verdadeiro na primeira vez que você entrar. O motivo é que o estado do nó predecessor é SIGNAL quando a condição de retornar true é verdadeira e o SIGNAL não foi definido para o nó predecessor na primeira vez. Somente após o CAS definir o estado, ele retornará true na segunda vez.

Então, qual é o significado de SIGNAL?

Referência aqui: Concurrent Programming-Detailed AQS CLH 锁 # Por que o AQS precisa de um nó principal virtual waitStatus é abreviado como ws aqui, e cada nó tem uma variável ws para indicar o status do nó. Quando inicializado, é 0. Se for cancelado, o sinal é -1. Se o estado de um nó for sinal, quando o nó liberar o bloqueio, ele precisará despertar o próximo nó. Portanto, antes de cada nó dormir, se o ws do nó predecessor não estiver definido para sinalizar, ele nunca será ativado. Portanto, descobriremos que quando o ws do nó da unidade atual acima for 0 ou propagar, use a operação cas para definir ws para sinalizar, de modo que o nó anterior possa notificar a si mesmo quando o bloqueio for liberado.

boolean parkAndCheckInterrupt ()

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

Depois que o método shouldParkAfterFailedAcquire retornar true, esse método será chamado para suspender o encadeamento atual.

O encadeamento suspenso pelo método LockSupport.park (this) pode ser ativado de duas maneiras: 1. por unpark () 2. por interrupt ().

Observe que Thread.interrupted () aqui limpará o bit do sinalizador de interrupção.

void cancelAcquire (nó)

Quando o tryAcquire acima não consegue adquirir o bloqueio, ele chegará a este método.

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
        }
    }

Liberar recursos

lançamento booleano (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) é um método de gancho fornecido pelo AQS para subclasses. As subclasses podem personalizar a maneira de liberar recursos exclusivamente. O lançamento é bem-sucedido e retorna verdadeiro, caso contrário, retorna falso.
  • O método unparkSuccessor (node) é usado para despertar o próximo thread que pode ser ativado na fila de espera, não necessariamente o próximo nó seguinte, por exemplo, ele pode estar em um estado cancelado.
  • O ws de cabeça não deve ser igual a 0, por quê? Quando um nó tenta se suspender, ele definirá o nó predecessor para SIGNAL -1. Mesmo se for o primeiro nó a se juntar à fila, após não conseguir obter o bloqueio, o ws definido pelo nó virtual será definido como SIGNAL , e este O julgamento é evitar que vários threads sejam liberados repetidamente. Então, também podemos ver a operação de definir ws como 0 ao liberar.

void unparkSuccessor (nó do nó)

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);
    }

Link original: https://www.cnblogs.com/summerday152/p/14244324.html

Se você acha que este artigo é útil para você, pode seguir minha conta oficial e responder à palavra-chave [Entrevista] para obter uma compilação de pontos de conhecimento do núcleo de Java e um pacote de presente para entrevista! Existem mais artigos técnicos e materiais relacionados para compartilhar. Deixe que todos aprendam e progridam juntos!

 

Acho que você gosta

Origin blog.csdn.net/weixin_48182198/article/details/112341438
Recomendado
Clasificación