Deep understanding of CountDownLatch

concept

CountDownLatch can be used to implement one or more threads waiting for other threads to complete a set of specific operations before continuing to run. This set of operations is called prerequisite operations

use

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时唤醒
}

operation result:

Insert picture description here
CountDownLatch internally maintains a counter that represents the number of outstanding operations . CountDownLatch.countDown() will decrease the counter value of the corresponding instance by one each time it is executed.
CountDownLatch.await() is a blocking method. When the counter value is 0, it means that all the prerequisite operations have been executed, and the blocked thread is awakened.

Source code analysis

Note: I will comment out the function of each method in the source code, you can refer to the comments for understanding.

First look at the main methods of the CountDownLatch class
Insert picture description here
and then look at its class structure diagram. The
Insert picture description here
CountDownLatch class is an independent class with only one internal class Sync, which inherits the AQS abstract class. As we said before, AQS is the implementation of all JUC locks and defines the basic operation of locks.
Here, we mainly analyze the two methods of await() and countDown() .
Let’s first look at the construction method of CountDownLatch.

Construction method

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

Constructing method of inner class Sync

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

Directly call the method of the parent class AQS to set the state value of the lock. As we said before, by controlling this variable, a shared shared lock or an exclusive lock can be realized.

await()

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

Previously, when we analyzed exclusive locks, we used the tryAcquire(arg) method to preempt the lock, but this time the shared lock is through the tryAcquireShared(arg) method.

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

Click to find that it is to determine the state value of the lock. If the state is 0, it means that the thread that is currently on-hook will be awakened; if it is not equal to 0, it means that the current thread cannot obtain the lock and needs to call doAcquireSharedInterruptibly(arg) to block and hang

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

First add the current node to the end of the AQS queue through the addWaiter method.
When the current node is the head node and the lock status is 0, execute the setHeadAndPropagate() method to wake up the thread

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

Here we need to talk about the difference between a shared lock and an exclusive lock when waking up:

  • Exclusive lock: When the lock is acquired by the head node, only the head node acquires the lock, and the threads of the other nodes continue to sleep, waiting for the lock to be released before awakening the thread of the next node.
  • Shared lock: As long as the head node successfully acquires the lock, it will wake up the thread corresponding to its own node while continuing to wake up the thread of the next node in the AQS queue. Each node will wake up the thread corresponding to the next node while waking up itself , In order to realize the "backward propagation" of the shared state, so as to realize the sharing function.

If the current node is not the head node, it needs to block and wait for the current thread. The logic here is the same as the ReentrantLock logic:

  • If the previous node of the current node is the head node, you can try to obtain the lock;
  • If not, you need to change the state of the previous node to SIGNAL, and then block and wait.
    For specific method analysis, refer to the previous article for a deep understanding of ReentrantLock.
    Looking at the await method, the implementation of the countDown method is almost the same.

countDown()

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

Take a look at the implementation of the tryReleaseShared method of the CountDownLatch class

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

The logic is very simple, get the state value of the lock

  • If the value of the starting state is 0, then return false;
  • If it is 0 after subtracting 1, it returns true

When the state is reduced to 0, it is necessary to wake up the blocking waiting thread

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

As long as the head node in the queue is not null, and is not equal to tail, and the state is SIGNAL , the state is changed to 0 through CAS, and if it succeeds, the current thread is awakened. The current thread will wake up in the doAcquireSharedInterruptibly method, try to acquire the lock again, and continue to wake up the next thread after the lock is acquired, just like a domino effect.

to sum up

  • After the internal counter value of CountDownLatch reaches 0, its value is constant, and any thread that continues to execute the await method will not be suspended
  • In order to avoid waiting for the thread to be suspended forever, the countDown() method must be placed where the code can always be executed, such as in the finally block

Guess you like

Origin blog.csdn.net/xzw12138/article/details/106501919