CountDownLatch简介
CountDownLatch是基于AQS实现的一个线程同步工具,也称为闭锁。它的作用是让一个或者多个线程等待某个事件的发生。简单的理解为CountDownLatch有一个正数计数器,countDown方法对计数器做减操作,await方法等待计数器达到0。所有await的线程都会阻塞直到计数器为0或者等待线程中断或者超时。
CountDownLatch唯一的构造方法CountDownLatch(int count),当在闭锁上调用countDown()方法时,闭锁的计数器将减1,当闭锁计数器为0时,闭锁将打开,所有等待的线程将通过闭锁开始执行。
下面例子是让两个线程等待另外两个线程。
public class Work implements Runnable{
private CountDownLatch downLatch;
private String name;
public Work(CountDownLatch downLatch, String name){
this.downLatch = downLatch;
this.name = name;
}
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + " done!");
this.downLatch.countDown();
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(2);
new Thread(new Work(latch,"four")).start();
new Thread(new Work(latch,"lc")).start();
new Thread(() -> {
try {
latch.await();
} catch (InterruptedException e) { }
System.out.println("waiting over!");
}).start();
latch.await();
System.out.println("all done");
}
}
实现原理
下面分析CountDownLatch的实现原理,先从它的构造方法入手。
public CountDownLatch(int count) {
this.sync = new Sync(count);
}
Sync继承了AQS,AQS中的state在这里表示计数值得大小,即等待几个线程的意思。
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
}
await
在前面的例子中,我们知道了 await 的作用是等待计数器减为0,否则一直阻塞,直到超时或者中断。这里我们分析无参的方法。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS
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。如果不为0,那么进入doAcquireSharedInterruptibly方法去挂起线程。
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);
}
}
这段代码和我们在AQS中分析acquireQueued方法逻辑基本是一样的,多了一步创建Node节点,并且Node是定义成共享的Node.SHARED,并且加入到同步队列中。被唤醒后重新尝试获取锁,不只设置自己为head,还需要通知其他等待的线程。尝试去获取锁,如果返回值大于0,表示获取成功,则去唤醒后面的节点。
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
setHead设置头节点后,取出下一个节点,如果也是共享类型,进行doReleaseShared释放操作。下个节点被唤醒后,重复上面的步骤,达到共享状态向后传播。
countDown
countDown是释放锁的过程,releaseShared在AQS中实现,tryReleaseShared是由CountDownLatch内部类Sync实现。
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
尝试释放锁,如果完全释放通知唤醒队列中的线程,完全释放指的是state减为0,同步队列中保存的是调用了await方法的线程。
private void doReleaseShared() {
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;
}
}
如果头结点的状态为SIGNAL,通过CAS置位0,并调用unparkSuccessor唤醒下个节点。被唤醒的节点状态会重置成0,在下一次循环中被设置成PROPAGATE状态,代表状态要向后传播。此方法的出口只有一个,即 h == head条件,当同步队列中没有可以唤醒的节点时,此条件成立。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里需要注意在AQS中,我们分析到Head节点是持有锁的线程,在此处,Head节点是没有线程与它对应,Head节点后面的都是阻塞线程。unparkSuccessor方法的作用是寻找下一个可用节点(status<0),将它的状态置位0,并且唤醒。