一:简介
CountDownLatch可以使一个或者多个线程等待其他线程都执行完毕后再执行。
CountDownLatch 定义了一个计数器,和一个阻塞队列, 当计数器的值递减为0之前,调用await()的线程处于挂起状态,当计数器递减到0时会唤醒挂起的线程,这里的计数器是一个标志,可以表示一个任务一个线程,也可以表示一个倒计时器,CountDownLatch可以解决那些一个或者多个线程在执行之前必须依赖于某些必要的前提业务先执行的场景。比如,过山车之类的,通常等位置坐满了,再运行。
CountDownLatch本身是基于共享锁实现的,共享锁我们上一篇分析过了,JUC并发基石之AQS源码解析–共享锁的获取与释放
二:源码分析
CountDownLatch主要是通过AQS的共享锁机制实现的,它的内部类Sync继承自AQS,同时覆写了tryAcquireShared和tryReleaseShared,以完成具体的实现共享锁的获取与释放的逻辑。
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;
}
}
}
再来看它的构造函数:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
在构造函数中,传入了任务数,作为AQS的state的初始值。
2.1 countDown()
对于countDown方法,每调用一次,就会将当前的count减一,当count值为0时,就会唤醒所有等待中的线程:
public void countDown() {
sync.releaseShared(1);
}
调用的AQS的releaseShared():
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
我们来看看CountDownLatch中tryReleaseShared(arg)的实现:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 获取state的值
int c = getState();
// 为0说明已经用过了,返回false
if (c == 0)
return false;
int nextc = c-1;
// CAS将状态减一,减一后的state为1, 返回true
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
首先获取当前的state值,如果已经为0了,直接返回false;否则通过CAS操作将state值减一,之后返回的是nextc == 0,
由此可见,该方法只有在count值原来不为0,但是调用后变为0时,才会返回true,否则返回false,并且,该方法在返回true之后,后面如果再次调用,还是会返回false。这就是CountDownLatch只能使用一次的原因。
当tryReleaseShared返回true以后,就会调用doReleaseShared方法唤醒所有等待中的线程,这个方法我们已经详细分析过了,这里就不再赘述了。
感觉比较有意思的是,CountDownLatch只是使用了共享锁的框架实现了自己的逻辑,在tryReleaseShared并没有释放什么锁,只是把state的状态减一,当状态从一减为零的时候,才会返回true,使得阻塞队列上的线程会被唤醒。实际上我们完全可以自己设一个全局变量count来实现相同的效果,只不过对这个全局变量要使用CAS进行操作。
2.2 await()
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
内部调用的是acquireSharedInterruptibly方法,走的是获取共享锁的逻辑,而且是响应中断的。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 线程被中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取锁
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
在尝试获取锁的过程中,只是判断state的值是不是0,为没有真正的获取什么锁,它只是借用了共享锁的获取来实现自己的逻辑。
如果state不为0,那么调用doAcquireSharedInterruptibly(arg)将当前线程封装成Node,丢到sync queue中去阻塞等待,直到被唤醒,再去tryAcquireShared(arg)。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
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;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程挂起的过程中,被中断过,直接抛出中断异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可见,doAcquireSharedInterruptibly(arg)方法当线程挂起的过程中,被中断过,直接抛出中断异常,而doAcquireShared()方法是不响应中断的,只记录中断标志位,其他逻辑之前分析过了,这里不再赘述。
三:总结
CountDownLatch相当于一个“门栓”,一个“闸门”,只有它开启了,代码才能继续往下执行。通常情况下,如果当前线程需要等其他线程执行完成后才能执行,我们就可以使用CountDownLatch。
使用await方法会使线程阻塞,直到“闸门”的开启,它是看state是否为0来判断“闸门”是否开启,countDown方法减少闸门所等待的任务数,每调用一次countdown,state的值会减一,当state从一减为零的时候,才会真正的去唤醒阻塞队列的线程,而且CountDownLatch是一次性的,当state为零之后,便不能再唤醒阻塞队列的线程。