jdk版本:1.8
CountDownLatch是一个可以等待多个线程的并发包工具,远比thread.join好用的多,底层使用了AQS的技术,AQS使用了CAS乐观锁的技术,是一个多线程环境中常见工具。也就是说包含了并发包,多线程,AQS,CAS这些技术,全部都是面试中的常客,如果能搞懂CountDownLatch,对于面试和找工作还是会有一丢丢的帮助的。
//CountDownLatch.Class
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;
}
}
}
//CountDownLatch.Class
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
上述代码分别是CountDownLatch的内部类Sysc和构造方法,构造方法入参为int类型,表示要等待的线程数,这个值会保存在Sysc的state中,从上述代码中可以看到tryAcquireShared方法目的是判断state是否为0。
其实CountDownLatch类中一共就没有几个方法,最主要的几个分别是构造方法,countDown方法,一个无参数await方法和一个带超时时间的await方法。
//CountDownLatch.Class
public void countDown() {
sync.releaseShared(1);
}
//AbstractQueuedSynchronizer.Class
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
//CountDownLatch.Class
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;
}
}
//AbstractQueuedSynchronizer.Class
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
countDown方法从源码中我们能读出他的目的是降state中的值做减1操作,如果因为这个操作state等于0则会调用doReleaseShared()方法,释放共享锁。这里有一个compareAndSetState方法,这个方法继续向下看可以看出是unsafe.compareAndSwapInt这个方法,就是我们常说的cas,这个方法中的四个参数this代表这个被操作的对象,stateOffset代表state这个属性的偏移量,调用这个方法的时候jvm可以根据这个偏移量知道这个属性当前的值,expect这个参数代表预期值,update这个参数代表更新后的值,这个方法意思就是如果满足state==expect,则会将state的值更新为update,如果state!=expect则返回false,所以tryReleaseShared方法是一个死循环,会一直getstate这个值尝试进行更新操作,如果更新之前值被其他线程变更则再重新获取新的state值反复操作。至于doReleaseShared这个方法一会在await后再讲。光从语义上理解则是如果state==0,我们就会释放掉CountDownLacth的锁,让程序继续进行。
//CountDownLatch.Class
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//AbstractQueuedSynchronizer.Class
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
//AbstractQueuedSynchronizer.Class
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);
}
}
await方法我们能看出来一样调用了tryAcquireShared这个方法,这个方法小于0的条件是,没有进行-1操作之前state就已经为0,到这我们就能看出,如果state为0的情况下,调用await方法是不会有阻塞的,说明阻塞代码在doAcquireSharedInterruptibly这个方法中。这个方法中的具体细节不贴了,自己对照代码看看,能看出来在进入这个方法后还会做一次释放锁的尝试,如果尝试成功会调用setHeadAndPropagate这个方法,这个方法读进去之后会发现里边调用的还是上文没有讲的doReleaseShared得方法,并将head置为当前node,仅从语义中我们得知在await方法中一样可以通过判断state的值来释放锁。而如果state依然是大于0的值则head则会将节点的状态置为SIGNAL,并再下一次循环中调用LockSupport.park(this)这个方法将自己阻塞在这里,如果有线程调用了unpark,则会继续执行并再次循环。
//AbstractQueuedSynchronizer.Class
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
而doReleaseShared方法的目的正是将head节点的阻塞解除,使其正常执行,LockSupport.unpark(s.thread);,达到解锁的目的。
CountDownLatch的总体流程基本就是这样,无论是在调用countDown的时候将state改为0还是await的时候state已经为0都可以解除阻塞恢复运行,不会因为代码顺序导致死锁问题。在我们的项目中,会定义一个和线程数相同的CountDownLatch对象,每一个线程执行完会进行一次countDown,主线程会在线程池执行后进行一次有超时时间的countDownLacth.await操作,用于统计整个线程池中的失败成功数量和运行时间。至于有超时时间的await方法是怎么实现的,自己看看吧,拜拜。