Conocimiento profundo de CountDownLatch

concepto

CountDownLatch se puede usar para implementar uno o más subprocesos esperando que otros subprocesos completen un conjunto de operaciones específicas antes de continuar ejecutándose. Este conjunto de operaciones se denomina operaciones de requisitos previos.

utilizar

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch countDownLatch=new CountDownLatch(2);

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//2-1
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    new Thread(()->{
    
    
        System.out.println(Thread.currentThread().getName()+ "->begin");
        countDownLatch.countDown();//1-1=0
        System.out.println(Thread.currentThread().getName()+ "->end");
    }).start();

    countDownLatch.await();//阻塞,count=0时唤醒
}

resultado de la operación:

Inserte la descripción de la imagen aquí
CountDownLatch mantiene internamente un contador que representa el número de operaciones pendientes . CountDownLatch.countDown () disminuirá el valor del contador de la instancia correspondiente en uno cada vez que se ejecute.
CountDownLatch.await () es un método de bloqueo. Cuando el valor del contador es 0, significa que se han ejecutado todas las operaciones de prerrequisito y se despierta el hilo bloqueado.

Análisis de código fuente

Nota: Comentaré la función de cada método en el código fuente, puede consultar los comentarios para comprenderlo.

Primero mire los métodos principales de la clase CountDownLatch
Inserte la descripción de la imagen aquí
y luego observe su diagrama de estructura de clases La
Inserte la descripción de la imagen aquí
clase CountDownLatch es una clase independiente con una sola clase interna Sync, que hereda la clase abstracta AQS. Como dijimos antes, AQS es la implementación de todos los bloqueos JUC y define el funcionamiento básico de los bloqueos.
A continuación, se analizan principalmente los dos métodos de Await () y la cuenta atrás () .
Veamos primero en el método de construcción de CountDownLatch.

Método de construcción

public CountDownLatch(int count) {
    
    
//count不能小于0 ,否则会抛异常
 if (count < 0) throw new IllegalArgumentException("count < 0");
 	//调用内部类的构造方法
    this.sync = new Sync(count);
}

Método de construcción de sincronización de clases internas

 Sync(int count) {
    
    
 	//设置锁的state值
 	setState(count);
 }

Llame directamente al método de la clase padre AQS para establecer el valor de estado del bloqueo.Como dijimos antes, al controlar esta variable, se puede realizar un bloqueo compartido compartido o un bloqueo exclusivo.

esperar()

public void await() throws InterruptedException {
    
    
  sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
    throws InterruptedException {
    
    
    //判断线程的中断标记,interrupted默认是false
    if (Thread.interrupted())
        throw new InterruptedException();
        //判断锁的state值,
        //等于0,需要把当前线程唤醒
    if (tryAcquireShared(arg) < 0)
    	//不等于0,需要去把当前线程挂起,阻塞
        doAcquireSharedInterruptibly(arg);
}

Anteriormente, cuando analizamos el bloqueo exclusivo, usamos el método tryAcquire (arg) para adelantar el bloqueo, pero esta vez el bloqueo compartido es a través del método tryAcquireShared (arg) .

protected int tryAcquireShared(int acquires) {
    
    
   return (getState() == 0) ? 1 : -1;
}

Haga clic para encontrar que es para determinar el valor de estado del bloqueo. Si el estado es 0, significa que el subproceso que está actualmente colgado se activará; si no es igual a 0, significa que el subproceso actual no puede obtener el bloqueo y necesita llamar a doAcquireSharedInterruptiblemente (arg) para bloquear y colgar

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException 
    //将当前线程的节点加到AQS队列的尾部
    //并返回这个节点,也是队列的最后一个节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
    
    
    	//自旋
        for (;;) {
    
    
        	//找到当前节点的上一个节点
            final Node p = node.predecessor();
            //如果上一个节点是head,说明前面已经没有线程阻挡它获得锁
            if (p == head) {
    
    
            	//去获得锁的state值
                int r = tryAcquireShared(arg);
                //表示state值为0
                if (r >= 0) {
    
    
                	//将当前节点变为head节点
                	//开始传播,并且一个接一个将后面的线程都唤醒
                    setHeadAndPropagate(node, r);
                    //将当前节点踢出去,帮助gc清理
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            //获取不到锁,就需要阻塞当前线程
            //阻塞之前,需要改变前一个节点的状态。如果是 SIGNAL 就阻塞,否则就改成 SIGNAL
            //这是为了提醒前一个节点释放锁完后能够叫醒自己
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
    
    
        if (failed)
            cancelAcquire(node);
    }
}

Primero agregue el nodo actual al final de la cola AQS a través del método addWaiter .
Cuando el nodo actual es el nodo principal y el estado de bloqueo es 0, ejecute el método setHeadAndPropagate () para despertar el hilo

private void setHeadAndPropagate(Node node, int propagate) {
    
    
		//将当前节点变为head节点
        Node h = head; // Record old head for check below
        setHead(node);
       
       	//一个一个传播,将后面的线程全部唤醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
    
    
            Node s = node.next;
            //s==null是为了防止多线程操作时,再添加节点到为节点的时候,只来的及 node.prev = pred;
            //而 pred.next = node;还没执行的情况
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

Aquí tenemos que hablar sobre la diferencia entre un bloqueo compartido y un bloqueo exclusivo al despertar:

  • Bloqueo exclusivo: cuando el bloqueo es adquirido por el nodo principal, solo el nodo principal adquiere el bloqueo y los hilos de los nodos restantes continúan durmiendo, esperando que se libere el bloqueo antes de despertar el hilo del siguiente nodo.
  • Bloqueo compartido: siempre que el nodo principal adquiera el bloqueo con éxito, activará el hilo correspondiente a su propio nodo mientras continúa despertando el hilo del siguiente nodo en la cola AQS. Cada nodo activará el hilo correspondiente al siguiente nodo mientras se despierta a sí mismo. , Para realizar la "propagación hacia atrás" del estado compartido, para realizar la función de compartir.

Si el nodo actual no es el nodo principal, necesita bloquear y esperar el hilo actual. La lógica aquí es la misma que la lógica ReentrantLock :

  • Si el nodo anterior del nodo actual es el nodo principal, puede intentar obtener el bloqueo;
  • De lo contrario, debe cambiar el estado del nodo anterior a SIGNAL, luego bloquear y esperar.
    Para un análisis de métodos específicos, consulte el artículo anterior para obtener una comprensión profunda de ReentrantLock.
    En cuanto al método de espera , la implementación del método countDown es casi la misma.

cuenta regresiva()

public void countDown() {
    
    
   sync.releaseShared(1);
  }
  
public final boolean releaseShared(int arg) {
    
    
//将state值减1
 if (tryReleaseShared(arg)) {
    
    
 		//当state=0时执行
 		//去唤醒之前阻塞等待的线程
        doReleaseShared();
        return true;
    }
    return false;
}

Eche un vistazo a la implementación del método tryReleaseShared de la clase CountDownLatch

protected boolean tryReleaseShared(int releases) {
    
    
	//防止CAS失败,自旋
     for (;;) {
    
    
     	//获取锁的state
         int c = getState();
         if (c == 0)
             return false;
         int nextc = c-1;
         if (compareAndSetState(c, nextc))
             return nextc == 0;
     }
 }

La lógica es muy simple, obtenga el valor de estado de la cerradura

  • Si el valor del estado inicial es 0, devuelve falso;
  • Si es 0 después de restar 1, devuelve verdadero

Cuando el estado se reduce a 0, es necesario despertar el hilo de espera de bloqueo

private void doReleaseShared() {
    
    
 for (;;) {
    
    
     Node h = head;
     if (h != null && h != tail) {
    
    
         int ws = h.waitStatus;
         if (ws == Node.SIGNAL) {
    
    
             if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                 continue;            // loop to recheck cases
             unparkSuccessor(h);
         }
         else if (ws == 0 &&
                  !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
             continue;                // loop on failed CAS
     }
     if (h == head)                   // loop if head changed
         break;
 }
}

Siempre que el nodo principal de la cola no sea nulo, no sea igual a la cola y el estado sea SIGNAL , el estado cambia a 0 a través de CAS y, si tiene éxito, se despierta el hilo actual. El hilo actual se activará en el método doAcquireSharedInterruptbly e intentará adquirir el bloqueo de nuevo. Después de adquirir el bloqueo, continuará despertando el siguiente hilo, como un efecto dominó.

para resumir

  • Una vez que el valor del contador interno de CountDownLatch llega a 0, su valor es constante y no se suspenderá ningún subproceso que continúe ejecutando el método de espera.
  • Para evitar esperar a que el hilo se suspenda para siempre, el método countDown () debe colocarse donde el código siempre se pueda ejecutar, como en el bloque finalmente

Supongo que te gusta

Origin blog.csdn.net/xzw12138/article/details/106501919
Recomendado
Clasificación