JDK源码分析深入源码分析CountDownLatch
Posted d-homme
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK源码分析深入源码分析CountDownLatch相关的知识,希望对你有一定的参考价值。
前言
CountDownLatch是一个闭锁实现,它可以使一个或者多个线程等待一组事件发生。它包含一个计数器,用来表示需要等待的事件数量,coutDown方法用于表示一个事件发生,计数器随之递减,而await方法等待计数器为0之前一直阻塞。它是基于AQS的共享锁来实现的,其中使用了较多的AQS的方法,所以在这之前最好阅读过AQS的源码,不嫌弃也可以查看本人之前AQS的源码分析,有些AQS方法没有在之前分析过的这里涉及到了会进行分析。
源码
我们先看它的属性和构造器,
// Sync为其内部类 private final Sync sync; // 唯一的一个构造器 // 构造参数count就是需要等待事件的数量 public CountDownLatch(int count) { // 为了保证count >= 0 if (count < 0) throw new IllegalArgumentException("count < 0"); // 构造sync this.sync = new Sync(count); }
现在来看内部类Sync,它继承了AQS,实现了共享锁方法,下面来看其源码,代码行数不多很好理解
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { // setState 为AQS更改其state变量的方法 // 将AQS state变量设置成count setState(count); } int getCount() { // AQS的获取state锁状态值 return getState(); } // 尝试获取共享锁 protected int tryAcquireShared(int acquires) { // 返回1表示此时锁状态值为0表示锁已释放 // -1表示此时锁状态值大于0,表示出于锁定状态 return (getState() == 0) ? 1 : -1; } // 尝试释放共享锁(计数器递减releases次) protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero // 等待锁状态值为0或者更改锁状态值成功 for (;;) { // 将state赋值给变量c int c = getState(); if (c == 0) // 此时锁已清除 return false; // 递减 int nextc = c-1; // 比较state的状态值是否等于C,等于将state状态值改为nextc if (compareAndSetState(c, nextc)) // 更改成功后,如果nextc为0则返回true return nextc == 0; } } }
await方法
await方法就是当state状态值不为0时将当前线程阻塞,然后等待唤醒
public void await() throws InterruptedException { //调用的AQS获取共享锁可中断方法 sync.acquireSharedInterruptibly(1); }
我们来看看AQS的acquireSharedInterruptibly方法
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 此方法调用的是CountDownLatch内部类Sync的方法 // 如果锁状态不为0,则执行doAcquireSharedInterruptibly方法 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
doAcquireSharedInterruptibly方法也是由AQS实现的
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { // 添加一个共享锁节点到队列 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { // 直到线程被唤醒或者线程被中断时跳出循环 for (;;) { // node节点的前驱节点 final Node p = node.predecessor(); if (p == head) { // 调用CountDownLatch内部类Sync的方法 // 如果锁状态值为0,则返回值大于0 int r = tryAcquireShared(arg); if (r >= 0) { // 当锁状态值为0,开始将note节点设置为头节点并唤醒后继节点 // 也就是队列不断的出列,然后唤醒后继节点,后继节点被唤醒后由于前驱节点被设置成头节点,又会调用该方法进行后继节点的唤醒 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } /* shouldParkAfterFailedAcquire用于清除已中断/或者取消的线程以及判断此次循环是否需要挂起线程 parkAndCheckInterrupt 挂机当前线程 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 在AQS之前博文里分析过这里就不再分析了 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) // 表示当前线程中断,取消获取锁 // 之前分析过,略过源码分析 cancelAcquire(node); } }
setHeadAndPropagate方法,主要作用是唤醒后继节点线程
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // 当前节点设置为头节点,节点关联的线程设置为空 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()) // 节点等待状态为signal时,唤醒后继节点线程 doReleaseShared(); } }
doReleaseShared很巧妙,当当前节点等待状态为signal时,唤醒后继节点线程
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; // 当前线程等待状态为signal时表示后继节点需要唤醒 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 表示h节点的状态替换失败,会再次循环判断h节点的状态 continue; // loop to recheck cases // 唤醒后继节点 unparkSuccessor(h); } // 状态为0时,将其改成PROPAGATE,更改失败会再次循环判断h节点的状态
// 这种情况发生在一个线程调用await方法,节点的等待状态还是初始值0未来得及被修改,刚好state被置为0然后调用了doReleaseShared方法
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
countDown方法
countDown方法递减state值,当值为0时,依次唤醒等待的线程
public void countDown() { // 递减一次state值,知道state为0时唤醒等待中的线程 sync.releaseShared(1); }
public final boolean releaseShared(int arg) { // 尝试将state减去arg if (tryReleaseShared(arg)) { // state为0时唤醒线程 doReleaseShared(); return true; } return false; }
到此分析完毕。
总结
- 通过源码知道CountDownLatch 不能像CyclicBarrier那样使用完毕后还可以复用;
- CountDownLatch 是通过共享锁来实现的,它的构造参数就是AQS state的值;
- 由于内部类继承了AQS,所以它内部也是FIFO队列,同时也一样是前驱节点唤醒后继节点。
以上是关于JDK源码分析深入源码分析CountDownLatch的主要内容,如果未能解决你的问题,请参考以下文章
Java技术专题「源码分析系列」深入分析JDK动态代理的分析原理机制
深入了解JDK SPI的源码分析及实践使用方式,看完对你应该有所帮助