引言及简介

前面我们介绍了独占锁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的类结构

 

java 文件同步程序 java同步工具_子线程

 

 从上图可以看出,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的多个线程(如果存在多个线程的话)之间却不能得出可见性结论。