详解-CountDownLatch-底层原理

1,146 阅读5分钟

CountDownLatch功能分析

countdownlatch是一个同步工具类,它允许一个或多个程一直等待,直到其他线程的操作执行完毕再执行。从命名可以解读到countdown是倒数的意思,类似于我们倒计时的概念。

countdownlatch 提供了两个方法,一个是 countDown, 一个是await, countdownlatch初始化的时候需要传入一个整数,在这个整数倒数到 0 之前,调用了 await 方法的程序都必须要等待,然后通过countDown来倒数。

  • 案例分析

从代码的实现来看,有点类似join的功能,但是比join更加灵活。CountDownLatch 构造函数会接收一个 int 类型的参数作为计数器的初始值,当调用CountDownLatch的countDown方法时,这个计数器就会减一。 通过await方法去阻塞主流程。

  • 模拟一下高并发的场景

  • 我们可以看到,此时的1000个线程都被阻塞了,CountDownLatch设置为1,所以当调用一次 countDownLatch.countDown(),然后这1000个线程万马奔腾,并发的执行完毕。

总的来说,凡事涉及到需要指定某个人物在执行之前,要等到前置人物执行完毕之后才执行的场景,都可以使用 CountDownLatch 。

CountDownLatch 源码分析

对于 CountDownLatch,我们仅仅需要关心两个方法,一个是 countDown() 方法,另一个是 await() 方法。

countDown() 方法每次调用都会将 state 减 1,直到 state 的值为 0;

await() 是一个阻塞方法,当 state 减为 0 的时候,await 方法才会返回。await 可以被多个线程调用,大家在这个时候脑子里要有个图:所有调用了 await 方法的线程阻塞在 AQS 的阻塞队列中,等待条件满足(state == 0),将线程从队列中一个个唤醒过来。

  • await() 方法
  • acquireSharedInterruptibly() 方法

countdownlatch 也用到了 AQS,在 CountDownLatch 内部写了一个Sync并且继承了AQS这个抽象类重写了 AQS 中的共享锁方法。首先看到下面这个代码,这块代码主要是判断当前线程是否获取到了共享锁;(在 CountDownLatch 中,使用的是共享锁机制,因为 CountDownLatch并不需要实现互斥的特性)

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 判断当前线程是否被阻塞
        if (Thread.interrupted())
            throw new InterruptedException();
        //判断计数器是否 < 0
        if (tryAcquireShared(arg) < 0)
        	//加入到共享队列
            doAcquireSharedInterruptibly(arg);
    }
  • tryAcquireShared()方法 这个方法很简单,就是判断当前 CountDownLatch 对象的计数器是否为0,如果为0返回1,否则返回-1
     protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
     }
  • doAcquireSharedInterruptibly() 方法
  1. addWaiter设置为shared模式。
  2. tryAcquire和tryAcquireShared的返回值不同,因此会多出一个判断过程
  3. 在判断前驱节点是头节点后,调用了setHeadAndPropagate方法,而不是简单的更新一下头节点。
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建一个节点,添加进双向链表中
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
        	//自旋操作
            for (;;) {
            	//拿到 Node 的节点的 prev 指向的节点
                final Node p = node.predecessor();
                //如果 p 为 head 指向的节点
                if (p == head) {
                	//判断尝试获取锁
                    int r = tryAcquireShared(arg);
                    //如果> 0,表示获取到了执行权限
                    if (r >= 0) {
                    	//从双向链表中移除掉当前线程的节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //阻塞节点,判断是否有中断操作
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • addWaiter() 创建一个节点,并添加到 AQS 同步队列中
    private Node addWaiter(Node mode) {
    	//创建一个 Node 节点,设置状态为 SHARED
        Node node = new Node(Thread.currentThread(), mode);
        //添加进原本的双向链表中
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //创建双向链表
        enq(node);
        return node;
    }
  • 图解分析

加入这个时候有3个线程调用了await方法,由于这个时候state的值还不为0,所以这三个线程都会加入到AQS队列中。并且三个线程都处于阻塞状态

  • 接着分析 CountDownLatch.countDown()方法

由于线程被await方法阻塞了,所以只有等到 countdown方法使得state=0的时候才会被唤醒,我们来看看countdown做了什么

  1. 只有当 state 减为 0 的时候,tryReleaseShared 才返 回 true, 否则只是简单的 state = state - 1
  2. 如果 state=0, 则调用 doReleaseShared 唤醒处于 await 状态下的线程
    public void countDown() {
        sync.releaseShared(1);
    }
    
    public final boolean releaseShared(int arg) {
    	//判断计数器是否为0
        if (tryReleaseShared(arg)) {
        	//唤醒所有线程
            doReleaseShared();
            return true;
        }
        return false;
    }
  • tryReleaseShared()
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
  • doReleaseShared()

共享锁的释放和独占锁的释放有一定的差别前面唤醒锁的逻辑和独占锁是一样,先判断头结点是不是 SIGNAL状态,如果是,则修改为0,并且唤醒头结点的下一个节点

PROPAGATE :标识为 PROPAGATE 状态的节点,是共享锁模式下的节点状态,处于这个状态下的节点,会对线程的唤醒进行传播

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //如果头节点状态为 SIGNAL 
                if (ws == Node.SIGNAL) {
                	//再进行一次 CAS 判断 
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    //唤醒线程
                    unparkSuccessor(h);
                }
                // 这个 CAS 失败的场景是:执行到这里的时候,刚好有一个节点入队,入队会将 这个 ws 设置为 -1 
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果到这里的时候,前面唤醒的线程已经占领了 head,那么再循环。
            //通过检查头节点是否改变了,如果改变了就继续循环。 
            if (h == head)                  
                break;
        }
    }
  • 到这里 head 节点的线程已经被唤醒,然后就回到头节点线程阻塞的位置,唤醒剩下所有的线程
  • setHeadAndPropagate() 方法

这个方法的主要作用是把被唤醒的节点,设置成head节点。 然后继续唤醒队列中的其他线程。 由于现在队列中有3个线程处于阻塞状态,一旦ThreadA 被唤醒,并且设置为head之后,会继续唤醒后续的 ThreadB

 private void setHeadAndPropagate(Node node, int propagate) {
        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;
            if (s == null || s.isShared())
            	//唤醒下一个线程,然后下一个线程又会执行该方法
                doReleaseShared();
        }
    }