JUC框架 CountDownLatch源码解析 JDK8

前言

CountDownLatch是一种常用的JUC框架构件,它用在一组线程任务需要等到条件满足后才能从同一起跑线开始执行的场景。比如当你去吃饭,你已经坐在桌上了但还是不能动筷子,因为各个领导还没来,只有领导都到齐了,大家才能开吃;再比如,参加了一场不能提前交卷的考试,学霸已经早早做完了但也不能提前走,只有当时间流逝直到考试结束时,大家才能离开考场。

CountDownLatch一般构造器给定一个大于0的数n,当调用了nCountDown后,条件就将满足,所有阻塞在await()的线程将从同一起跑线开始执行。

JUC框架 系列文章目录

实现核心

CountDownLatch向下依赖了AQS的共享锁部分,它使用了一个内部类继承了AQS。既然是使用的共享锁,那么肯定要实现tryAcquireSharedtryReleaseShared方法,因为共享锁的获取和释放流程都会依赖子类对这两个方法的实现。

    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }

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

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

    private final Sync sync;

构造器

    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

构造器需要传入一个大于等于0的数,这个数将作为AQS的state的初始值。

核心方法

  1. countDown方法,每调用一次就将当前的计数器count减一,当count为0时,唤醒所有阻塞在await方法处的线程。
  2. await方法。当count不为0时,调用await的线程将阻塞。它有两种版本:普通版本、带超时的版本。两个版本都响应超时。

CountDownLatch具体的讲,依赖了AQS的共享锁模式的sync queue

countDown()

//CountDownLatch
    public void countDown() {
        sync.releaseShared(1);
    }

countDown方法没有参数,因为每次调用只能将count减少1。

//AbstractQueuedSynchronizer
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {  //返回值很重要,直接影响能否执行doReleaseShared
            doReleaseShared();
            return true;
        }
        return false;
    }

tryReleaseShared的返回值很重要,直接影响能否执行doReleaseShared。而doReleaseShared的逻辑就是“唤醒所有阻塞在await方法处的线程”。

//CountDownLatch
        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;
            }
        }
  • 获得当前AQS的state
    • 如果state是0,那么不能再减了直接返回false,因为state最多能减到0。
    • 如果state不是0,需要CAS设置state。
      • CAS操作成功了,返回nextc == 0
      • 当然CAS操作可能失败,失败了就需要再次循环执行CAS,因为可能同时有多个线程在同时执行countDown
  • CAS操作成功返回的是nextc == 0nextc为CAS设置成功后,state的新值。也就是说,只有那个将state从1设置为0的线程,才会返回true,其他调用countDown的线程都会返回false。

回到releaseShared的逻辑,只有tryReleaseShared(arg)返回了true,doReleaseShared()才会执行,才会去唤醒所有阻塞在await方法处的线程。关于共享锁的流程之前已经讲过,我们只需要知道doReleaseShared()会唤醒sync queue中的head后继,而被唤醒的线程tryAcquireShared成功后在一定条件下也会去调用doReleaseShared()唤醒它的后继,这样可能会有多个线程同时执行doReleaseShared()。重点在于,唤醒线程的速度很快,几乎可以算是同时进行的。

一直在说唤醒sync queue中的head后继,那head后继的代表线程到底阻塞在await()执行过程的哪里了呢,带着这一问题,我们进入下一章。

await()

首先它的效果和Condition接口的await()类似,一般调用之后线程就会陷入阻塞。而当count为0时,线程从await()处被唤醒而继续执行。从下面的函数声明就可以看出,CountDownLatch的await()是响应中断的。

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())  //上来先检查一下,当前线程的中断状态
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)  //如果Acquire失败,就需要走循环阻塞的流程了
        doAcquireSharedInterruptibly(arg);
}

我们来看看CountDownLatch的AQS子类是怎么实现tryAcquireShared的。

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

发现有点欺骗观众的感觉,因为传入的参数acquires根本没有使用,逻辑只是检查AQS的state是否为0。如果state为0返回大于0的数,如果state不为0返回小于0的数。

共享锁的流程中我们提到过tryAcquireShared返回值的含义:

tryAcquireShared子类实现判断tryAcquireShared返回值tryAcquireShared返回值含义await流程
state为0> 0获取共享锁成功,并且后续获取也可能获取成功将返回,不阻塞
-= 0获取共享锁成功,但后续获取可能不会成功将返回,不阻塞
state不为0< 0获取共享锁失败将阻塞

所以,只要tryAcquireShared返回了 > 0的数,acquireSharedInterruptibly就直接返回了不会阻塞了,因为此时state已经为0了,说明已经调用了足够次数的countDown了。如果tryAcquireShared返回了 < 0的数,acquireSharedInterruptibly就需要调用下面的doAcquireSharedInterruptibly,将当前线程包装成node扔到suyc queue上去,走循环 抢锁->阻塞 的流程了。

    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {  //不同之处1
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            /*boolean interrupted = false;*/    //不同之处2
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        /*if (interrupted)    //不同之处3
                            selfInterrupt();*/  
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();  //不同之处4
            }
        } finally {
            if (failed)
                cancelAcquire(node);  //不同之处5
        }
    }

之前在共享锁的流程中已经讲过了doAcquireShared,而doAcquireSharedInterruptibly和它也很类似,所以我们看看不同之处就好。

  1. 函数声明是抛出异常的,因为现在的doAcquireSharedInterruptibly是响应中断的版本。
  2. 此处在doAcquireSharedInterruptibly中被删除掉了,因为此函数在遇到中断时就直接抛出异常,所以不需要局部变量来保存是否有过中断的信息。
  3. 此处在doAcquireSharedInterruptibly中被删除掉了,分析同上。被删掉的此处在doAcquireShared中的作用是返回用户代码前设置一下中断状态。
  4. 线程阻塞在parkAndCheckInterrupt后,如果因为中断而被唤醒,parkAndCheckInterrupt会返回true,然后就直接抛出异常就好。
  5. 此处其实一模一样。但是在doAcquireShared中此处永远不可能执行到,在doAcquireSharedInterruptibly中如果因为抛出异常而退出函数,则会执行此处。

回想之前提的问题,“head后继的代表线程到底阻塞在await()执行过程的哪里”,可以看到,线程在tryAcquireShared失败后,一定会阻塞在parkAndCheckInterrupt这里的。所有的调用CountDownLatch.await()阻塞的线程,都是阻塞在这里的。

而当那个调用countDown从而将count从1变成0的线程执行完countDown后,会唤醒sync queue中的head后继线程,已经说了线程是阻塞在parkAndCheckInterrupt这里,所以head后继线程会从parkAndCheckInterrupt处唤醒,然后继续下一次循环,执行tryAcquireShared会返回>0的数(此时count已经为0了),然后执行setHeadAndPropagate,在里面然后又会执行doReleaseShared

现在好了,不仅调用countDown从而将count从1变成0的线程会执行doReleaseShared,调用await()的线程被唤醒后也会执行doReleaseShared,之后被唤醒的线程也会去执行doReleaseShared。这样不断唤醒,很快在sync queue上的所有线程都会被唤醒。之所以说它,是因为每个尝试获取锁(即tryAcquireShared动作)的线程,在获取锁完毕后并退出await()时就已经唤醒了自己的后继,当然,这本来就是共享锁获取的流程。

await(long timeout, TimeUnit unit)

await(long timeout, TimeUnit unit)的版本提供了超时功能。

public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
}

同样的,如果tryAcquireShared返回了小于0的数,就只好调用doAcquireSharedNanos走循环 抢锁->阻塞 的过程。但注意,上面两个函数都是有返回值的。

await(long timeout, TimeUnit unit)返回值返回值含义
true在规定时间内,抢到了锁 ==> 在规定时间内,count变成了0
false在规定时间内,没抢到锁 ==> 在规定时间内,count没变成0

很明显,如果tryAcquireShared返回了大于等于0的数,就会直接返回true了;如果tryAcquireShared返回了小于0的数,具体返回的值就等于doAcquireSharedNanos(arg, nanosTimeout)的返回值了。

private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {  //不同之处1
    if (nanosTimeout <= 0L)  //不同之处2
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;  //不同之处3
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
            }
            nanosTimeout = deadline - System.nanoTime();  //不同之处4
            if (nanosTimeout <= 0L)  //不同之处5
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)  //不同之处6
                LockSupport.parkNanos(this, nanosTimeout);  //不同之处7
            if (Thread.interrupted())  //不同之处8
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

直接看看doAcquireSharedNanos(int arg, long nanosTimeout)doAcquireSharedInterruptibly(int arg)的不同之处吧。

  1. 返回值是boolean的,含义解释过了。
  2. 如果发现传来的时间段是小于0的,也就说明用户想要等待0的时间(反过来说,就是不需要等待),也就不用进入下面的循环了。进入doAcquireSharedNanos后还没有尝试抢过锁,所以返回false。
  3. 计算出等待的截止时间。
  4. 计算出离截止时间还有多久。
  5. 如果发现当前时间已经超过了截止时间,则直接返回false,不用再去抢锁了。
  6. 虽然没超过截止时间,但离截止时间已经很近了,就不要再阻塞了,直接自旋就好了。因为考虑到阻塞唤醒,时间太短反而无法控制。
  7. 超时版本需要调用LockSupport.parkNanos
  8. 因为从上面的LockSupport.parkNanos处被中断唤醒时,不会带有信息(LockSupport.parkNanos没有返回值),所以需要检测一下中断状态。

简单的说,doAcquireSharedNanos(int arg, long nanosTimeout)正常返回时,有两种情况:因为获得锁而返回true,因为超时还没获得锁而返回false。

对比两个await方法返回时的情况

返回原因等到了count变成0count未变成0的任何时候,(超时前)中断来临超时还没等到count为0
await()只要是正常返回的抛出中断异常-
await(long timeout, TimeUnit unit)正常返回的,且返回值为true抛出中断异常正常返回的,且返回值为false

两个await方法,我们都可以从返回的情况,就可以知道返回的原因。

分析两种线程

现将await()await(long timeout, TimeUnit unit)都统称为await方法,那么关于CountDownLatch有两种线程:

  • 调用countDown的线程,肯定不会阻塞。
  • 调用await的线程,会阻塞。

对于CountDownLatch的使用者来说,是这样的使用场景:需要当某个条件满足后,才让某些任务从同一个起跑线开始执行,而“满足条件”则被量化为一个数字。

  • 当更加接近这个条件时,让第一类线程调用countDown,也可以调用多次。
  • 需要放到同一个起跑线的线程任务,则调用await。它们将几乎同时被唤醒。

当然,这也不是绝对的,完全可以一个线程同时充当两个角色,那么它将先调用countDown,再调用await。如果每个线程都这样使用,那么使用CountDownLatch就相当于使用了一个一次性的CyclicBarrier。

总结

  • CountDownLatch的使用场景:当某个量化为数字的条件被满足后,几个线程任务才可以同时开始执行。
  • 调用CountDownLatch#await的线程将等待条件被满足,条件满足后,调用CountDownLatch#await的若干线程将从同一个时间点继续执行。
  • 调用CountDownLatch#countDown,让量化为数字的条件减一。
  • 调用CountDownLatch#await的线程,和调用CountDownLatch#countDown的线程,是两类线程,并无关系。
  • 唤醒阻塞在await的若干线程的过程很快,这是由于共享锁的特性导致:共享锁获取成功时,也会唤醒sync queue的后继节点线程,但独占锁可不这样。
  • CountDownLatch依赖了共享锁模式节点的sync queue
  • CountDownLatch是一次性的,count为0无法改变。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值