1. 是什么

不能重复使用的计数器。让一个线程等待其他线程完事再往下执行,类似于Thread.join()

底层使用AQS实现

2. 如何使用

public class CountDownLatchTest
{
public static void main(String[] args) throws InterruptedException
{
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++)
{
int finalI = i;
new Thread(() -> {
try
{
if (finalI == 5)
{
TimeUnit.SECONDS.sleep(10L);
}
System.out.println(String.format("线程%s,时间【%s】 countdown", Thread.currentThread().getName(),LocalDateTime.now())); latch.countDown();
System.out.println(String.format("线程%s,时间【%s】 执行完毕", Thread.currentThread().getName(),LocalDateTime.now())); }
catch (InterruptedException e)
{
e.printStackTrace();
} }).start();
} latch.await();
System.out.println(Thread.currentThread().getName() + "开始执行");
}
}
  • 注意

    这里countdown的线程不会互相等待,谁先执行完谁就先退出

2.1. CountDownLatch VS CyclicBarrier

CountDownLatch CyclicBarrier
使用场景 一个线程等待其他线程执行完毕,再往下执行 所有线程相互等待直到最后一个线程到达,再往下执行
能否重复使用 不可以 可以
底层实现 AQS Lock+Condition

3. uml

4. 构造方法

public class CountDownLatch {

	//继承了AQS
private final Sync sync; public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
//默认就设置了count个信号量(即相当于一开始就加锁了count次)
this.sync = new Sync(count);
} }

4.1. Sync【AQS子类】

private static final class Sync extends AbstractQueuedSynchronizer {

    Sync(int count) {
setState(count);
} int getCount() {
return getState();
} //重写的是AQS共享获取锁的方法
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
} //重写的是AQS共享释放锁的方法
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;
}
}
}

5. countDown方法

public void countDown() {
//调用了AQS的releaseShared方法
sync.releaseShared(1);
}

5.1. 使用AQS释放锁

  • AQS releaseShared
public final boolean releaseShared(int arg) {
//调用Sync重写的tryReleaseShared释放信号量
if (tryReleaseShared(arg)) {
//释放锁成功后调用Sync的doReleaseShared方法
doReleaseShared();
return true;
}
return false;
}
  • 3行:调用AQS的tryReleaseShared方法释放锁,由于Sync重写了这个方法,所以调用的是Sync重写的tryReleaseShared释放锁。当锁的数量减为0返回ture,表明所有线程都准备就绪
  • 5行:使用tryReleaseShared释放锁成功后调用Sync的doReleaseShared方法。移除AQS队列中SIGNAL的节点并一个个唤醒

下面具体说明:

5.1.1. 尝试释放锁

  • Sync.tryReleaseShared
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
//不断尝试
for (;;) {
//信号量为0,表明还没有人加锁,自然没法解锁,返回失败
int c = getState();
if (c == 0)
return false;
//CAS设置信号量-1。
int nextc = c-1;
if (compareAndSetState(c, nextc))
//看是否为0,是则返回成功
return nextc == 0;
}
}

5.1.2. 所有锁释放成功后,移除AQS队列中SIGNAL的节点,并一个个唤醒

  • doReleaseShared
private void doReleaseShared() {
//不断尝试
for (;;) { Node h = head;
//AQS队列不为空,把队列中SIGNAL的节点移除
if (h != null && h != tail) {
int ws = h.waitStatus;
//头节点状态为SIGNAL
if (ws == Node.SIGNAL) {
//在头节点状态为signal的情况设置为0,失败了继续直到成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//把头节点从AQS队列中移除
unparkSuccessor(h);
}
//头节点状态为0,那么设置为PROPAGATE,失败了继续直到成功
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
} //队列中没有SIGNAL的节点
if (h == head) // loop if head changed
break;
}
}
5.1.2.1. 把头节点从AQS队列中移除
  • unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
//当前节点的状态<0,则把状态改为0
//0是空的状态,因为node这个节点的线程释放了锁后续不需要做任何
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //当前节点的下一个节点为空或者状态>0(即是取消状态)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//那么从队尾开始往前遍历找到离当前节点最近的下一个状态<=0的节点(即非取消状态)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//唤醒下一个节点
if (s != null)
LockSupport.unpark(s.thread);
}

6. await方法

public void await() throws InterruptedException {
//调用AQS的acquireSharedInterruptibly方法加锁
sync.acquireSharedInterruptibly(1);
}

6.1. 使用AQS加锁

  • AQS acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//调用Sync重写的tryAcquireShared判断是否加锁。
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
  • 6行:调用Sync重写的tryAcquireShared判断是否需要加锁,而不是真的加锁。可以看出当tryAcquireShared返回<0的时候需要往下执行doAcquireSharedInterruptibly进行加锁。

    而tryAcquireShared返回<0指的是一开始设置的count个信号量没有被用完,说明其他线程任务没执行完
  • 7行:加锁。准确说是加入AQS队列,阻塞等待其他线程执行完

下面详细说明:

6.1.1. 判断是否需要加锁

  • Sync tryAcquireShared
protected int tryAcquireShared(int acquires) {
//当前锁的数量为0,即所有线程任务都执行完了,那么返回1不用加锁
//否则>0指的是一开始设置的count个信号量没有被用完,说明其他线程任务没执行完。那么该线程需要进行加锁,故返回-1
return (getState() == 0) ? 1 : -1;
}

6.1.2. 需要加锁,那么加入AQS队列阻塞等待其他线程执行完

当state>0说明有信号量没被释放完,那么需要加锁

  • doAcquireSharedInterruptibly
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//以SHARE模式加入AQS队列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//死循环直到获取锁成功
for (;;) {
//逻辑1.
//当前节点的前一个节点是头节点的时候(公平锁:即我的前面没有人等待获取锁),尝试获取锁
final Node p = node.predecessor();
if (p == head) {
//state == 0(即没人加锁的情况下才执行加锁--其实并没有真的加锁)
int r = tryAcquireShared(arg);
if (r >= 0) {
//获取锁成功后设置头节点为当前节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//逻辑2.
//当前节点的前一个节点状态时SIGNAL(承诺唤醒当前节点)的时候,阻塞当前线程。
//什么时候唤醒?释放锁的时候
//唤醒之后干什么?继续死循环执行上面的逻辑1
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
//如果发生了异常,那么执行下面的逻辑
} finally {
//除了获取锁成功的情况都会执行cancelAcquire方法
if (failed)
cancelAcquire(node);
}
}
6.1.2.1. 构造节点加入AQS队列
  • AQS.addWaiter
 private Node addWaiter(Node mode) {
//用当前线程、SHARED模式构造节点
Node node = new Node(Thread.currentThread(), mode);
// 队列不为空
Node pred = tail;
if (pred != null) {
//插入到队尾
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//队列为空或者插入到队尾失败
enq(node);
return node;
}

队列为空或者插入到队尾失败的情况执行enq,如下

  • AQS.enq
private Node enq(final Node node) {
//死循环直到入队成功
for (;;) {
Node t = tail;
//队列为空,那么初始化头节点。注意是new Node而不是当前node(即队头是个占位符)
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
//队列不为空,插入到队尾
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
6.1.2.2. 判断是否需要阻塞
  • shouldParkAfterFailedAcquire
//根据(前一个节点,当前节点)->是否阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//前一个节点的状态时SIGNAL,即释放锁后承诺唤醒当前节点,那么返回true可以阻塞当前线程
if (ws == Node.SIGNAL)
return true;
//前一个节点状态>0,即CANCEL。
//那么往前遍历找到没有取消的前置节点。同时从链表中移除CANCEL状态的节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 前置节点状态>=0,即0或者propagate。
//这里通过CAS把前置节点状态改成signal成功获取锁,失败的话再阻塞。why?
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
6.1.2.3. 真正阻塞
  • parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
//使用Unsafe阻塞当前线程,这里会清除线程中断的标记,因此需要返回中断的标记
LockSupport.park(this);
return Thread.interrupted();
}

6.1.3. 不需要加锁

当state=0说明所有信号量已被释放完,那么直接返回,执行业务逻辑

7. 总结

  • 让一个线程等待其他线程完事再往下执行,类似于Thread.join()
  • 主线程创建CountDownLatch的时候初始化了信号量,相当于一开始就有N个人加锁。
  • 主线程调用await的时候检查信号量是否为0,不为0说明其他线程没有执行完,那么加入AQS队列阻塞,等待唤醒
  • 其他线程调用countDown的时候会使信号量-1,最后一个线程减为0的时候会唤醒AQS队列中的所有节点(主线程),让其继续往下执行
  • 主线程被唤醒继续往下执行

8. 参考

Java源码分析系列笔记-9.CountDownLatch的更多相关文章

  1. Java源码分析系列之HttpServletRequest源码分析

    从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...

  2. Java源码分析系列

    1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...

  3. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  4. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  5. [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat

    概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...

  6. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  7. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  8. spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析

    更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...

  9. spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...

  10. spring源码分析系列 (3) spring拓展接口InstantiationAwareBeanPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.InstantiationAwareBeanPostProcessor简述与demo示例 二.InstantiationAwareBean ...

随机推荐

  1. SpreadJS V18.0 新版本发布!数据驱动革新,效率与体验全面升级

    表格控件SpreadJS推出V18.0及V8.0版本!本次更新聚焦数据管理.多语言适配.报表与透视表增强,新增多项重磅功能,赋能企业高效应对复杂业务场景.核心亮点速览 一.表格绑定数据源:直连数据管理 ...

  2. CVE-2025-29927 Next.js 中间件权限绕过漏洞复现

    漏洞信息 Next.js 是一个基于 React 的流行 Web 应用框架,提供服务器端渲染.静态网站生成和集成路由系统等功能.包含众多功能,是深入研究复杂研究的完美游乐场.在信念.好奇心和韧性的推动 ...

  3. 实现领域驱动设计 - 使用ABP框架 - 领域逻辑 & 应用逻辑

    领域逻辑 & 应用逻辑 如前所述,领域驱动设计中的业务逻辑分为两部分(层):领域逻辑和应用逻辑: 领域逻辑由系统的核心领域规则组成,应用逻辑实现应用特定的用例 虽然定义很明确,但实现起来可能并 ...

  4. 【Markdown】简明语法手册

    Cmd Markdown 简明语法手册 标签: Cmd-Markdown 1. 斜体和粗体 使用 * 和 ** 表示斜体和粗体. 示例: 这是 *斜体*,这是 **粗体** 这是 斜体,这是 粗体. ...

  5. zk源码—3.单机和集群通信原理

    大纲 1.单机版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 2.集群版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 (3)Leader选举阶段 (4)Leader和Fol ...

  6. 💻开源项目介绍-NewsNow-优雅的实时新闻聚合平台

    news.zktww.vip 引言 在信息洪流中,如何优雅地获取新闻? 在当今信息爆炸的时代,我们每天需要在微博.知乎.Twitter.GitHub等平台间频繁切换,才能捕捉到最新的热点动态. New ...

  7. 🎀GitHub Pages静态文件发布

    简介 GitHub Pages是GitHub提供的一项服务,允许用户和组织从存储库中的静态文件创建和托管网站.这些静态文件可以是HTML.CSS.JavaScript文件或任何其他可以在浏览器中直接渲 ...

  8. ESP32S3播放音频文件

    ESP32S3播放音频文件 硬件基于立创实战派esp32s3 软件代码基于立创实战派教程修改,分析 播放PCM格式音频 原理图分析 音频芯片ES8311 ES8311_I2C_ADD:0x18 音频功 ...

  9. 说说 Java 的执行流程?

    Java 的执行流程 Java 的执行流程包括多个阶段,从源码编写到最终程序的执行,涉及到编译.类加载.字节码执行.垃圾回收等多个环节.下面将详细介绍 Java 程序的执行流程. 1. 编写源代码 开 ...

  10. strftime()函数的用法

    strftime()函数的用法 strftime()函数可以把YYYY-MM-DD HH:MM:SS格式的日期字符串转换成其它形式的字符串.strftime()的语法是strftime(格式, 日期/ ...