1.什么是CountDownLatch?
建议先看这篇文章,里面有部分方法之前的文章里面写过,就没有重复写了:从源码去分析J.U.C核心之AQS。
CountDownLatch是一个工具类,它允许一个或者多个线程一直等待,直到其他线程的操作执行完毕再执行,从命名可以解读到CountDown是倒数的意思,类使用我们倒计时的概念。
我们来看如下一段代码:注意到一共有两个线程,线程里面分别去执行countDown,CountDownLatch我们传入的值是3。这样永远看不到输出“线程执行完毕”这句话。
当我们在代码中再加入一个线程去执行,就能看到输出了“线程执行完毕”。那么我们可以发现这里类似于一个倒计时,每次调用countDown就会数一次,当我们预先设定的值数完之后,就会执行await之后的代码。
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
new Thread(()->{
countDownLatch.countDown();
});
new Thread(()->{
countDownLatch.countDown();
});
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程执行完毕");
}
我们可以从下面的流程图来对它进一步了解
2.源码分析
首先,我们看下类的关系图:
CountDownLatch的内部定义了一个抽象类Sync继承了AbstractQueuedSynchronizer。这里主要用到了AQS里面一个共享锁的功能。
我们看下它的一个初始化构造函数,
这里面创建 了一个Sync的实例
方法里面又把传进来得到count值给了AQS的state,我们知道state为0表示没有锁的状态,大于0表示没有锁的状态。可以参考我的另外一篇文章从源码去分析J.U.C核心之AQS。每次调用countDown方法其实是对state进行递减,当值为0时,await方法才会被唤醒。接下来我们看看await里面做了什么。
2.1 await方法
可以看到这个方法里面其实是调用了Sync的acquireSharedInterruptibly方法,这里是去获得一个共享锁的过程。
可以看到这里有一个tryAccquireShared方法,需要判断它的返回值是否小于0
显然,只有当state值为0时,才会返回1,什么时候才会为0?? countDown
方法执行了设置的次数之后才会为0。也就是说,当我们处于阻塞时,这里肯定是返回的-1,那么此时会进入doAcquireSharedInterruptibly
方法。
可以看到这里通过addWaiter定义了一个Node节点,addWaiter在另一篇文章中有介绍,它是封装了一个Node节点,并加到AQS队列里面。通过自旋,先拿到当前节点的上一个节点,如果上个节点是头节点,这里会尝试去获得state的状态值,此时因为countDown方法还未执行到指定次数,这里返回的是-1。这里会调用shouldParkAfterFailedAcquire方法,把它的前置节点ws修改成-1,并将当前节点挂起,变为阻塞状态。
2.2 countDown方法
这里会调用Sync的releaseShared方法去修改state的值,传的参数是1
这里通过自旋的方式去获得state的值,如果等于0,就返回false,否则就会用之前的state值去减1。
当最后一次执行该方法时,nextc就等于0了,此时通过compareAndSetState方法将state值改为0,并返回true。从上面可以看出,当返回true时,会继续执行doReleaseShared方法。这里按照我们的猜测,它应该回去进行一个唤醒的操作。我们继续看下代码。
首先,拿到了头节点,进行了判断,显然,此时的头节点是不等于空的,并且也不等于尾结点。此时去获取它的一个waitState值,这个值在shouldParkAfterFailedAcquire方法中已经修改成了-1,也就是Node.SIGNAL。然后再尝试把这个值更新成0,如果返回false就跳出 当前循环,如果为true就唤醒此时的头节点的next节点。
可以看到,当前循环的结束条件为if (h == head)
这里可能会有疑问,因为最开始通过Node h = head;
把head节点赋值给了h,那么,这个条件不是肯定成立的吗?这里我们看下第一个if条件。if (h != null && h != tail) { ,那么,意味着,我们能执行到最后的这个if条件,肯定是head为空或者head==tail,为空肯定是不可能的,所以,这里表示的所有的节点都已经被成功唤醒。