CountDownLatch(倒计时器)源码解读与使用

简介: CountDownLatch(倒计时器)源码解读与使用

1. 前言

在很多的面经中都看到过提问 CountDownLatch 的问题,正好我们最近也在梳理学习AQS(抽象队列同步器)、CAS操作等知识,而 CountDownLatch 又是JUC包下一个比较常见的同步工具类,我们今天就继续来学一下这个同步工具类!


2. CountDownLatch有什么用

我们知道AQS是专属于构造锁和同步器的一个抽象工具类,基于它Java构造出了大量的常用同步工具,如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue等等,我们今天的主角CountDownLatch同样如此。


CountDownLatch(倒时器)允许N个线程阻塞在同一个地方,直至所有线程的任务都执行完毕。CountDownLatch 有一个计数器,可以通过countDown()方法对计数器的数目进行减一操作,也可以通过await()方法来阻塞当前线程,直到计数器的值为 0。


这个就很类似我们的Moba类游戏的游戏加载过程,所有玩家几乎是一起进入游戏,加载快的玩家要等待加载慢的玩家,只有当全部玩家加载完成才能进入游戏,而CountDownLatch就类似于这个过程中的发令枪。


3. CountDownLatch底层原理


想要迅速了解一个Java类的内部构造,或者使用原理,最快速直接的办法就是看它的源码,这是很多初学者比较抵触的,会觉得很多封装起来的源码都晦涩难懂,诚然很多类内部实现是复杂,我也是慢慢从刚开始阅读Mybatis源码,到后来阅读JDK多线程相关的源码,尝试培养自己看源码的习惯,硬着头皮看段时间还是有不少收获的。


好的,我们直接进入CountDownLatch内部去看看它的底层原理吧

//几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS
private static final class Sync extends AbstractQueuedSynchronizer {
     private static final long serialVersionUID = 4982264981922014374L;
     Sync(int count) {
         setState(count);
     }
     int getCount() {
         return getState();
     }
 }
private final Sync sync;
//构造方法中初始化count值
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}


是不是很熟悉?对又是Sync,与 Semaphore信号量一样,几乎所有基于AQS构造的同步类,内部都需要一个静态内部类去继承AQS

3.1. countDown()方法

//核心方法,内部封装了共享模式下的线程释放
 public void countDown() {
  //内部类Sync,继承了AQS
     sync.releaseShared(1);
 }
 //AQS内部的实现
 public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
      //唤醒后继节点
         doReleaseShared();
         return true;
     }
     return false;
 }   

在CountDownLatch中通过countDown来减少倒计时数,这是最重要的一个方法,我们继续跟进源码看到它通过releaseShared()方法去释放锁,这个方法是AQS内部的默认实现方法,而在这个方法中再一次的调用了tryReleaseShared(arg),这是一个AQS的钩子方法,方法内部仅有默认的异常处理,真正的实现由CountDownLatch内部类Sync完成,如下

// 对 state 进行递减,直到 state 变成 0;
// 只有 count 递减到 0 时,countDown 才会返回 true
protected boolean tryReleaseShared(int releases) {
    // 自选检查 state 是否为 0
    for (;;) {
        int c = getState();
        // 如果 state 已经是 0 了,直接返回 false
        if (c == 0)
            return false;
        // 对 state 进行递减
        int nextc = c-1;
        // CAS 操作更新 state 的值
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

当这个tryReleaseShared函数返回true时,也就是state扣减到了零,就会调用doReleaseShared唤醒CLH队列中阻塞等待的线程

3.2. await()方法

除了countDown()方法外,在CountDownLatch中还有一个重要方法就是await,在多线程环境下,线程的执行顺序并不一致,因此,对于一个倒时器也说,先开始的线程应该阻塞等待直至最后一个线程执行完成,而实现这一效果的就是await()方法!

// 等待
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// 带有超时时间的等待
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}


其中await()方法可以配置带有时间参数的,表示最大阻塞时间,当调用 await() 的时候,我们会调用aqs的一个模板方法acquireSharedInterruptibly(arg),如下:

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }


Thread.interrupted()为判断线程书否为中断状态,如果为中断状态,抛出中断异常,否则会调用tryAcquireShared(arg)方法,tryAcquireShared方法为AQS的钩子函数,由静态内部类Snyc实现,如下

1. proprotected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }


也就是说,当前state = 0就返回1,也就不会执行doAcquireSharedInterruptibly(arg),直接放行没有堵塞。否则会执行doAcquireSharedInterruptibly(arg)这个方法,这个方法内部主要是将当前线程加入CLH队列阻塞等待。


4. CountDownLatch的基本使用

由于await的实现步骤和countDown类似,我们就不贴源码了,大家自己跟进去也很容易看明白,我们现在直接来一个小demo感受一下如何使用CountDownLatch做一个倒时器

public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个倒计数为 3 的 CountDownLatch
        CountDownLatch latch = new CountDownLatch(3);
 
        Thread service1 = new Thread(new Service("3", 1000, latch));
        Thread service2 = new Thread(new Service("2", 2000, latch));
        Thread service3 = new Thread(new Service("1", 3000, latch));
 
        service1.start();
        service2.start();
        service3.start();
 
        // 等待所有服务初始化完成
        latch.await();
        System.out.println("发射");
    }
 
    static class Service implements Runnable {
        private final String name;
        private final int timeToStart;
        private final CountDownLatch latch;
 
        public Service(String name, int timeToStart, CountDownLatch latch) {
            this.name = name;
            this.timeToStart = timeToStart;
            this.latch = latch;
        }
 
        @Override
        public void run() {
            try {
                Thread.sleep(timeToStart);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name);
            // 减少倒计数
            latch.countDown();
        }
    }
}


输出结果

3
2
1
发射

执行结果体现出了倒计时的效果每隔1秒进行3,2,1的倒数;其实除了倒计时器外CountDownLatch还有另外一个使用场景:实现多个线程开始执行任务的最大并行性


多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。


具体做法是: 初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("5位运动员就位!");
                    //等待发令枪响
                    countDownLatch.await();
 
                    System.out.println(Thread.currentThread().getName() + "起跑!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        // 裁判准备发令
        Thread.sleep(2000);
        //发令枪响
        countDownLatch.countDown();
    }
}

输出结果

5位运动员就位!
5位运动员就位!
5位运动员就位!
5位运动员就位!
5位运动员就位!
Thread-0起跑!
Thread-3起跑!
Thread-4起跑!
Thread-1起跑!
Thread-2起跑!

5. 总结

CountDownLatch 是一个多线程同步辅助类,它允许一个或多个线程等待一系列操作在其他线程中完成。这个机制类似于一场赛跑,选手们在起跑线准备,等待发令枪响后才能开始比赛。在 CountDownLatch 的场景中,线程们等待一个共同的信号,只有当计数器降至零时,它们才能继续执行。


CountDownLatch 提供了两个主要方法:countDown() 和 await()。countDown()方法用于将计数器减一,而 await() 方法会阻塞调用线程,直到计数器达到零。这种机制确保了所有线程都会等待必要的操作完成。


内部实现上,CountDownLatch 通过一个静态内部类 Sync 继承自 AbstractQueuedSynchronizer(AQS)。AQS 提供了一个框架,用于构建自定义的同步器。在 CountDownLatch 中,Sync 类通过重写 AQS 的钩子方法 tryReleaseShared() 和 tryAcquireShared() 来实现其同步机制。


tryReleaseShared() 方法用于在共享模式下尝试释放资源

tryAcquireShared() 方法用于在共享模式下尝试获取资源

我们可以发现,几乎所有基于AQS构造的同步类,实现原理都是差不多的,都是通过维护AQS中被volatile修饰的state变量作为竞态条件来实现线程同步。

相关文章
|
存储 Java
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
AQS(AbstractQueuedSynchronizer,队列同步器)源码解读
|
域名解析 缓存 网络协议
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
|
人工智能 数据挖掘
🔔阿里云百炼智能体和工作流可以发布为组件了,AI应用变成“搭积木”
本文介绍了如何通过智能体组件化设计快速生成PPT。首先,创建一个“PPT大纲生成”智能体并发布为组件,该组件可根据用户输入生成结构清晰的大纲。接着,在新的智能体应用中调用此组件与MCP服务(如ChatPPT),实现从大纲到完整PPT的自动化生成。整个流程模块化、复用性强,显著降低AI开发门槛,提升效率。非技术人员也可轻松上手,满足多样化场景需求。
922 0
|
XML 机器学习/深度学习 JSON
|
7月前
|
人工智能 弹性计算 运维
简单快捷部署 | Bolt.diy 一步搞定创意建站
Bolt.diy 是 Bolt.new 的开源版本,提供全栈开发支持与自然语言交互功能,简化开发流程并允许二次开发。通过阿里云 CAP 平台部署,结合百炼大模型服务和 deepseek-v3 实现代码生成,用户可专注于应用创新。部署前需确保主账户资金充足,完成部署后配置 API-Key 即可使用。支持模型选择、代码下载等功能,适用于快速建站与创意开发需求。
161 10
|
存储 搜索推荐 数据库
软件系统【标签tag功能】的两种数据库设计
软件系统中的标签功能可采用两种数据库设计。方案一,文章和Tag各一表,Tag信息存储在文章表内(`tags`和`tagids`字段),优点是模型简单,但查询效率低且易引发数据冗余和一致性问题。方案二,增加Tagmap表,用于存储标签-文章映射,利于索引查询和数据更新,适用于高效率需求,但结构更复杂。
818 0
软件系统【标签tag功能】的两种数据库设计
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
532 4
Java ArrayList扩容的原理
|
设计模式 网络协议 Java
09.外观模式设计思想
外观模式设计思想:为了解决软件系统中的复杂性和耦合性问题,外观模式提供了一组统一的接口,简化客户端与子系统的交互。主要内容包括外观模式的基础、实现、实例演示、应用场景、优缺点及适用环境。通过创建外观类,将复杂系统的内部实现细节隐藏起来,只暴露出简化的接口给客户端,从而降低耦合度,提高系统的可维护性和可扩展性。[更多内容](https://yccoding.com/)
141 2
|
人工智能 自然语言处理 算法
大模型+蒙特卡洛树搜索,一招让LLaMa-3 8B奥数水平直逼GPT-4
【6月更文挑战第25天】 - 复旦大学和上海AI Lab的研究者提出这一算法,用于增强大型语言模型在复杂数学推理任务中的能力,解决现有模型推理准确性问题。 - **MCTSr**流程包括初始化、选择、自细化、自评估、反向传播和UCT更新,通过多轮迭代提升答案质量。 - 实验显示,该算法在**GSM8K**、**GSM Hard**、**MATH**和**Olympiad-level**数据集上表现出色,尤其在多次迭代后。 - 尽管计算成本高且不适用于所有问题类型,但研究揭示了强化LLMs推理能力的新途径,对未来的AI应用具有指导意义。
432 8
|
存储 NoSQL 关系型数据库
PostgreSQL中的变长数据结构Varlena
在本文是PostgreSQL学习系列---基础数据类型的第二篇,对PG中的Varlena类型进行了介绍,内容还会随着学习进行不断补充~
1838 0