引言及简介
前面我们介绍了独占锁ReentrantLock实现的一个同步辅助工具CyclicBarrier, 它能够使一组线程互相等待,今天我们介绍另一种同步辅助器CountDownLatch,它其实可以看着是利用共享锁实现的,只不过它没有使用到类似共享锁Semaphore那么复杂的逻辑,所以它的实现没有直接利用Semaphore完成,而是直接在AQS的共享式获取/释放同步资源的基础上实现的一个非常简单的同步辅助工具。
根据Java Doc的描述,CountDownLatch可以在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。类比CyclicBarrier的一组线程之间的相互等待,CountDownLatch则是一个或一组线程等待另一个或另一组线程,所以CountDownLatch与CyclicBarrier各自等待的线程是不同的。并且CountDownLatch内部维护的计数器是不能重置循环使用的,而CyclicBarrier的计数器是可以被重置循环使用的。
通俗的讲,在用给定的计数初始化CountDownLatch之后,所有执行了await()方法的线程必须要等待其他线程执行相应次数的countDown()之后才会返回,否则将一直阻塞。相当于CountDownLatch维护了一个计数器,执行了await()方法的线程必须要等到计数器清零之后才会返回,而其他线程每执行一次countDown(),计数器就会减1,所以其实执行await()方法的线程可以是多个,而执行countDown()的线程也可以是一个线程的多次执行,因为countDown()方法的执行不存在阻塞等待的情况。
使用示例
假设某个主任务在执行过程中,分别需要满足2个条件才能继续往下执行,这两个条件分别由另外两个线程去满足:
1 final CountDownLatch latch = new CountDownLatch(2);
2
3 new Thread(){
4 public void run() {
5 try {
6 System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
7 Thread.sleep(3000);
8 System.out.println("主线程"+Thread.currentThread().getName()+"达成主线程的条件1");
9 latch.countDown();
10 System.out.println("子线程"+Thread.currentThread().getName()+"继续执行");
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 };
15 }.start();
16
17 new Thread(){
18 public void run() {
19 try {
20 System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
21 Thread.sleep(5000);
22 System.out.println("子线程"+Thread.currentThread().getName()+"达成主线程的条件2");
23 latch.countDown();
24 System.out.println("子线程"+Thread.currentThread().getName()+"继续执行");
25 } catch (InterruptedException e) {
26 e.printStackTrace();
27 }
28 };
29 }.start();
30
31 try {
32 System.out.println("等待2个子线程达成主线程需要的2个条件...");
33 latch.await();
34 System.out.println("2个子线程都达成了主线程需要的条件,主线程继续执行");
35 } catch (InterruptedException e) {
36 e.printStackTrace();
37 }
执行结果:
1 子线程Thread-0正在执行
2 等待2个子线程达成主线程需要的2个条件...
3 子线程Thread-1正在执行
4 主线程Thread-0达成主线程的条件1
5 子线程Thread-0继续执行
6 子线程Thread-1达成主线程的条件2
7 2个子线程都达成了主线程需要的条件,主线程继续执行
8 子线程Thread-1继续执行
由上面的示例可以看到,CountDownLatch初始化为2,所以需要执行两次countDown()才会返回,否则就一直等到,示例中就是主线程等待2两个子线程,而两个子线程之间不存在相互影响或等待,子线程Thread-0执行了3秒钟就达成了条件1,然后继续做自己的事情,子线程Thread-1却花了5秒钟才达成条件2,然后主线程立即继续往下执行。两个子线程之间不存在相互等待或影响。
源码分析
首先,看看CountDownLatch的类结构
从上图可以看出,CountDownLatch非常简单,没有实现和继承任何接口和父类,直接将操作都代理到继承了AQS的静态内部类Sync上。CountDownLatch对外提供的最主要的方法就await/await(timeout)以及countDown():
1 public void await() throws InterruptedException {
2 sync.acquireSharedInterruptibly(1);
3 }
4 //直到超时,被中断,或者成功获取到同步资源才返回
5 //如果在超时到达之前成功获取资源返回true,否则返回false
6 //该返回值的含义其实也表示是否在超时到达之前,其他线程全部都达到指定的位置(即都执行了countDown()方法将计数器减到0)
7 public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
8 return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
9 }
10 //释放一个同步资源
11 public void countDown() {
12 sync.releaseShared(1);
13 }
14
15 public long getCount() {
16 return sync.getCount();
17 }
从源码可以发现,CountDownLatch确实利用的AQS的共享锁的逻辑,await必须要获取到一个同步资源才能返回,而countDown则是释放一个同步资源,接着看Sync的逻辑:
1 private static final class Sync extends AbstractQueuedSynchronizer {
2 private static final long serialVersionUID = 4982264981922014374L;
3
4 Sync(int count) { //初始化方法,即初始化CountDownLatch时传入的计数器
5 setState(count);
6 }
7
8 int getCount() { //获取当前剩余的同步资源个数
9 return getState();
10 }
11 //await方法最终的调用次方法尝试获取同步资源
12 protected int tryAcquireShared(int acquires) {
13 return (getState() == 0) ? 1 : -1; //只有当剩余同步资源为0时才表示成功。
14 }
15 //countDown方法最终的调用方法-释放同步资源
16 protected boolean tryReleaseShared(int releases) {
17 // Decrement count; signal when transition to zero
18 for (;;) {
19 int c = getState();
20 if (c == 0) //已经清0,直接返回false
21 return false;
22 int nextc = c-1;//直接做减1操作,如果成功,并且减到0才返回true,这样才会唤醒调用await阻塞的线程
23 if (compareAndSetState(c, nextc))
24 return nextc == 0;
25 }
26 }
27 }
由以上的源码可以很清楚CountDownLatch的实现,其原理大致是,使用CountDownLatch的构造方法传入一个正整形数字的参数之后,由AQS维护这相同数量的同步资源个数,调用await的线程除非在线程被中断,等待超时(如果有超时时间的话),或者该同步资源个数为0的时候才会返回,否则一直等待。而当有线程执行了countDown方法之后,如果当前同步资源个数不为0就减1,每执行一次countDown就减一次1,直到减到0才会唤醒等待的线程。减到0之后,如果继续执行countDown则不会有任何反应。
所以,当await()方法因为其他线程执行了countDown将计数器减至0被唤醒之后,再次调用await()方法时,肯定是会立即返回的,不论有没有执行countDown,因为其计数器一旦被清0,将无法被重新还原,这也就是CountDownLatch不能被重用的原因。
内存可见性
由于CountDownLatch其内部机制其实就是对声明在AQS中的volatile修饰的state变量的维护,所以CountDownLatch也自然满足volatile语义带来的happens-before原则,即“对一个volatile变量的写操作先行发生于后面对这个变量的读操作”, 因此可以得出,在其他线程中调用countDown(写volatile变量)之前的操作happens-before另一个线程执行await(读volatile变量)返回的操作。
也就是说,在其他线程执行countDown之前对共享变量的修改对执行await的线程在await方法返回之后是立即可见的。而那些执行countDown的多个线程(如果存在多个线程的话)之间却不能得出可见性结论。