专栏首页JavaEdgeCountDownLatch 核心源码解析
原创

CountDownLatch 核心源码解析

1 基本设计

一种同步辅助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。

CountDownLatch 是用给定的 count 初始化的。由于调用了countDown()方法,await 方法阻塞,直到当前计数为零,之后释放所有等待线程,并立即返回任何后续的 await 调用。这是一种一次性现象——计数无法重置。如果需要重置计数的版本,可以考虑使用CyclicBarrier

CountDownLatch 是一种通用的同步工具,可以用于多种用途。count为1时初始化的CountDownLatch用作简单的 on/off 的 latch或gate:所有调用wait的线程都在gate处等待,直到调用countDown()的线程打开它。一个初始化为N的CountDownLatch可以用来让一个线程等待,直到N个线程完成某个动作,或者某个动作已经完成N次。

CountDownLatch的一个有用的特性是,它不需要调用倒计时的线程等待计数达到0才继续,它只是防止任何线程继续等待,直到所有线程都通过。

2 类架构

2.1 UML 图

2.2 继承关系

可以看出,CountDownLatch并无显式地继承什么接口或类。

2.3 构造函数细节

  • 构造一个用给定计数初始化的CountDownLatch。
  • 参数 count 在线程通过await()之前必须调用countDown()的次数

CountDownLatch 的 state 并不是 AQS 的默认值 0,而是可赋值的,就是在 CountDownLatch 初始化时,count 就代表了 state 的初始化值

  • new Sync(count) 其实就是调用了内部类 Sync 的如下构造函数

count 表示我们希望等待的线程数,可能是

  • 等待一组线程全部启动完成,或者
  • 等待一组线程全部执行完成

2.4 内部类

和 ReentrantLock 一样,CountDownLatch类也存在一个内部同步器 Sync,继承了 AbstractQueuedSynchronizer

  • 这也是唯一的属性
    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) { // 降低计数器; 至 0 时发出信号 for (;;) { // 获取锁状态 int c = getState(); // 锁未被任何线程持有 if (c == 0) return false; int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }

3 await

可以叫做等待,也可以称之为加锁。

3.1 无参

造成当前线程等待,直到锁存器计数到零,除非线程被中断。

如果当前计数为零,则此方法立即返回。

如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于睡眠状态,直到发生以下两种情况之一:

  • 由于调用了countDown()方法,计数为零
  • 其他线程中断了当前线程

如果当前线程:

  • 在进入此方法时已设置其中断状态;或者
  • 在等待时被中断

就会抛 InterruptedException,并清除当前线程的中断状态。

无参版 await 内部使用的是 acquireSharedInterruptibly 方法,实现在 AQS 中的 final 方法

  1. 使用CountDownLatch 的内部类 Sync 重写的tryAcquireShared 方法尝试获得锁,如果获取了锁直接返回,获取不到锁走 2
  2. 获取不到锁,用 Node 封装一下当前线程,追加到同步队列的尾部,等待在合适的时机去获得锁,本步已完全实现在 AQS 中

tryAcquireShared

3.2 超时参数

  • 最终都会转化成毫秒
    造成当前线程等待,直到锁存器计数到零,除非线程被中断,或者指定的等待时间已过。 如果当前计数为零,则此方法立即返回值 true。

如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于休眠状态,直到发生以下三种情况之一:

  • 由于调用了countDown()方法,计数为零;或
  • 其他一些线程中断当前线程;或
  • 指定的等待时间已经过了

如果计数为零,则该方法返回值true。

如果当前线程:

  • 在进入此方法时已设置其中断状态;或
  • 在等待时中断,

就会抛出InterruptedException,并清除当前线程的中断状态。

如果指定的等待时间过期,则返回false值。如果时间小于或等于0,则该方法根本不会等待。

  • 使用的是 AQS 的 tryAcquireSharedNanos 方法

获得锁时,state 的值不会发生变化,像 ReentrantLock 在获得锁时,会把 state + 1,但 CountDownLatch 不会

4 countDown

降低锁存器的计数,如果计数为 0,则释放所有等待的线程。

如果当前计数大于零,则递减。如果新计数为零,那么所有等待的线程都将重新启用,以便进行线程调度。

如果当前计数等于0,则什么也不会发生。

releaseShared 已经完全实现在 AQS

主要分成两步:

  1. 尝试释放锁(tryReleaseShared),锁释放失败直接返回,释放成功走 2,本步由 Sync 实现
  2. 释放当前节点的后置等待节点,该步 AQS 已经完全实现tryReleaseShared

对 state 进行递减,直到 state 变成 0;当 state 递减为 0 时,才返回 true。

总结

研究完 CountDownLatch 的源码,可知其底层结构仍然依赖了 AQS,对其线程所封装的结点是采用共享模式,而 ReentrantLock 是采用独占模式。可以仔细对比差异,深入理解研究。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JDK源码解析实战 - CountDownLatch

    CountDownLatch 是用给定的 count 初始化的。由于调用了countDown()方法,await 方法阻塞,直到当前计数为零,之后释放所有等待线...

    JavaEdge
  • Java线程池-ThreadPoolExecutor源码解析(基于Java8)

    所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务.

    JavaEdge
  • JVM源码分析之synchronized1 字节码实现2 偏向锁

    javap命令生成的字节码中包含 ** monitorenter ** 和 ** monitorexit **指令

    JavaEdge
  • JDK源码解析实战 - CountDownLatch

    CountDownLatch 是用给定的 count 初始化的。由于调用了countDown()方法,await 方法阻塞,直到当前计数为零,之后释放所有等待线...

    JavaEdge
  • 【你问我答】这些Java并发问题,专家是这么回答的

    针对上期Java高并发【你问我答】中读者提出的问题,王锐同学的回答如下。 一 ---- 美团内部使用过Akka么?有什么坑? ——Absurd “ 答: 只简...

    美团技术团队
  • Java多线程编程-(13)-从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力

    对于Java来说我们知道,Java代码首先会编译成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上进行执行。

    Java后端技术
  • Vista 及后续版本的新线程池

    在上一篇的博文中,说了下老版本的线程池,在Vista之后,微软重新设计了一套线程池机制,并引入一组新的线程池API,新版线程池相对于老版本的来说,它的可控性更高...

    Masimaro
  • 让人头大的各种锁,从这里让你思绪清晰

    说到了锁我们经常会联想到生活中的锁,在我们日常中我们经常会接触到锁。比如我们的手机锁,电脑锁,再比如我们生活中的门锁,这些都是锁。

    乱敲代码
  • Java 锁分类

    乐观锁是一种乐观思想,认为读多写少,遇到并发的可能性低,每次拿数据时候并不会上锁,因为认为不会被别人修改。但是更新的时候会判断有没有人会更新这条数据,采取写的时...

    Yif
  • Java并发编程的艺术(六)——线程间的通信

    多条线程之间有时需要数据交互,下面介绍五种线程间数据交互的方式,他们的使用场景各有不同。 1. volatile、synchronized关键字 PS:关于vo...

    大闲人柴毛毛

扫码关注云+社区

领取腾讯云代金券