JAVA并发之CountDownLatch
前言
之前我们将ReentranLock从锁层面到AQS源码层面分析了如何构建一个所需的同步器,使用AQS需要实现哪些方法。ReentranLock的CLH队列中每个节点都是独占(EXCLUSIVE)的,那么节点的另一种等待方式——共享(SHARED)又将是什么样子的呢?本篇博客让我们通过另一个同步器——闭锁(CountDownLatch)来了解一下有什么不同的吧。
CountdownLatch的作用和主要流程:
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减1。当计数器的值为0时,表示所有的线程都已经完成了任务,然后在CountDownLatch上等待的线程就可以恢复执行任务。
CountdownLatch的例子:
这里我用了一个jdk源码中的例子:
1 class Driver { // ... 2 void main() throws InterruptedException { 3 CountDownLatch startSignal = new CountDownLatch(1);//设置启动闭锁 4 CountDownLatch doneSignal = new CountDownLatch(N);//完成闭锁 5 for (int i = 0; i < N; ++i) // create and start threads 6 new Thread(new Worker(startSignal, doneSignal)).start(); 7 doSomethingElse(); // 主线程做一些准备工作(先于其他线程) 8 startSignal.countDown(); // 启动闭锁-1(解除work的await等待) 9 doSomethingElse(); 10 doneSignal.await(); // wait for all to finish 11 } 12 } 13 class Worker implements Runnable { 14 private final CountDownLatch startSignal; 15 private final CountDownLatch doneSignal; 16 Worker(CountDownLatch startSignal, CountDownLatch doneSignal) { 17 this.startSignal = startSignal; 18 this.doneSignal = doneSignal; 19 } 20 public void run() { 21 try { 22 startSignal.await();//等待主线程做完准备工作 23 doWork(); 24 doneSignal.countDown();//结束后将结束闭锁-1 25 } catch (InterruptedException ex) { 26 } // return; 27 } 28 void doWork() { ... } 29 }
CountDownLatch源码:
CountdownLatch源码不多(毕竟AQS把底层的一些需求都封装好啦),来看看代码吧:
1 //核心类Sync,继承自AQS 2 private static final class Sync extends AbstractQueuedSynchronizer { 3 private static final long serialVersionUID = 4982264981922014374L; 4 Sync(int count) { 5 setState(count); 6 } 7 int getCount() { 8 return getState(); 9 } 10 //共享的尝试获取资源,如果state为0,则说明可以解除封锁,否则,返回-1进入阻塞 11 protected int tryAcquireShared(int acquires) { 12 return (getState() == 0) ? 1 : -1; 13 } 14 15 //共享的释放资源 16 protected boolean tryReleaseShared(int releases) { 17 // Decrement count; signal when transition to zero 18 for (;;) { 19 int c = getState(); 20 if (c == 0) 21 return false; 22 int nextc = c-1; 23 if (compareAndSetState(c, nextc)) 24 return nextc == 0;//当释放资源(-1)后state为0则说明可以解除封锁 25 } 26 } 27 } 28 private final Sync sync; 29 30 public CountDownLatch(int count) { 31 if (count < 0) throw new IllegalArgumentException("count < 0"); 32 this.sync = new Sync(count); 33 } 34 35 //在state变为0之前一直等待,除非被中断 36 public void await() throws InterruptedException { 37 sync.acquireSharedInterruptibly(1); 38 } 39 40 public boolean await(long timeout, TimeUnit unit) 41 throws InterruptedException { 42 return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); 43 } 44 45 //计数器减1 46 public void countDown() { 47 sync.releaseShared(1); 48 } 49 50 public long getCount() { 51 return sync.getCount(); 52 }
以上基本就是CountdownLatch的全部源码了,我们先从await()方法中深入到AQS,根据上面的代码,await直接调用了acquireSharedInterruptibly()方法:
1 public final void acquireSharedInterruptibly(int arg) 2 throws InterruptedException { 3 if (Thread.interrupted())//如果线程被中断,就往外抛 4 throw new InterruptedException(); 5 if (tryAcquireShared(arg) < 0)//如果state不为0 6 doAcquireSharedInterruptibly(arg); 7 }
如果未被中断的情况下并且state不为0(即闭锁要将线程锁住),我们再来看其中的doAcquireSharedInterruptibly方法做了什么呢,可以从CountdownLatch的作用猜测是将线程放入CLH队列中park等待休息,看看源码是否是我们猜测的那样吧:
1 private void doAcquireSharedInterruptibly(int arg) 2 throws InterruptedException { 3 final Node node = addWaiter(Node.SHARED); 4 boolean failed = true; 5 try { 6 for (;;) { 7 final Node p = node.predecessor(); 8 if (p == head) { 9 int r = tryAcquireShared(arg);//如果state不为0,r会一直小于0(不断自旋) 10 if (r >= 0) { 11 setHeadAndPropagate(node, r); 12 p.next = null; // help GC 13 failed = false; 14 return; 15 } 16 } 17 if (shouldParkAfterFailedAcquire(p, node) && 18 parkAndCheckInterrupt())//进入等待状态 19 throw new InterruptedException(); 20 } 21 } finally { 22 if (failed) 23 cancelAcquire(node); 24 } 25 }
这个方法是不是很眼熟?和Reentranlock中用到的doAcquireInterruptibly方法类似,不断自旋,尝试获取资源(tryAcquireShared),获取成功就返回,失败就判断是否需要休息。具体的细节不再展开讨论了,在第一篇AQS概述中我们就讨论了,AQS底层只要求同步器实现一种tryAcquire/tryRelease,根据是否共享(shared)是否需要响应中断(Interruptibly)组合成四对,因此完整看完一个AQS的同步器,再查看其他同步器时,基本是一样的流程。
我们继续看看countDown方法做了什么吧:
1 public void countDown() { 2 sync.releaseShared(1); 3 } 4 public final boolean releaseShared(int arg) { 5 if (tryReleaseShared(arg)) {//如果state变为0 6 doReleaseShared(); 7 return true; 8 } 9 return false; 10 } 11 private void doReleaseShared() {//把CLH队列中的所有signal的下一个节点都unpark 12 for (;;) { 13 Node h = head; 14 if (h != null && h != tail) { 15 int ws = h.waitStatus; 16 if (ws == Node.SIGNAL) { 17 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) 18 continue; // loop to recheck cases 19 unparkSuccessor(h); 20 } 21 else if (ws == 0 && 22 !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 23 continue; // loop on failed CAS 24 } 25 if (h == head) // loop if head changed 26 break; 27 } 28 }
countDown方法会尝试将state减一,如果state为0,则进入doReleaseShared方法,将所有在CLH队列中等待的节点全都unpark(因为是共享的节点)。
总结:
本篇CountDownLatch简单的对其源码进行了分析,对于一些主要的方法,我都在代码上加了简单的注释,对AQS有一定了解的人一定能够看懂其中的流程以及操作。