死磕java同步系列之countdownlatch源码解析(代码片段)

tong-yuan tong-yuan     2022-12-20     249

关键词:

??欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

(手机横屏看源码更方便)


问题

(1)CountDownLatch是什么?

(2)CountDownLatch具有哪些特性?

(3)CountDownLatch通常运用在什么场景中?

(4)CountDownLatch的初始次数是否可以调整?

简介

CountDownLatch,可以翻译为倒计时器,但是似乎不太准确,它的含义是允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作。

CountDownLatch的通常用法和Thread.join()有点类似,等待其它线程都完成后再执行主任务。

类结构

技术图片

CountDownLatch中只包含了Sync一个内部类,它没有公平/非公平模式,所以它算是一个比较简单的同步器了。

这里还要注意一点,CountDownLatch没有实现Serializable接口,所以它不是可序列化的。

源码分析

内部类Sync

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) 
        // 注意,这里state等于0的时候返回的是1,也就是说count减为0的时候获取总是成功
        // state不等于0的时候返回的是-1,也就是count不为0的时候总是要排队
        return (getState() == 0) ? 1 : -1;
    
    // 尝试释放锁
    protected boolean tryReleaseShared(int releases) 
        for (;;) 
            // state的值
            int c = getState();
            // 等于0了,则无法再释放了
            if (c == 0)
                return false;
            // 将count的值减1
            int nextc = c-1;
            // 原子更新state的值
            if (compareAndSetState(c, nextc))
                // 减为0的时候返回true,这时会唤醒后面排队的线程
                return nextc == 0;
        
    

Sync重写了tryAcquireShared()和tryReleaseShared()方法,并把count存到state变量中去。

这里要注意一下,上面两个方法的参数并没有什么卵用。

构造方法

public CountDownLatch(int count) 
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);

构造方法需要传入一个count,也就是初始次数。

await()方法

// java.util.concurrent.CountDownLatch.await()
public void await() throws InterruptedException 
    // 调用AQS的acquireSharedInterruptibly()方法
    sync.acquireSharedInterruptibly(1);

// java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly()
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException 
    if (Thread.interrupted())
        throw new InterruptedException();
    // 尝试获取锁,如果失败则排队
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);

await()方法是等待其它线程完成的方法,它会先尝试获取一下共享锁,如果失败则进入AQS的队列中排队等待被唤醒。

根据上面Sync的源码,我们知道,state不等于0的时候tryAcquireShared()返回的是-1,也就是说count未减到0的时候所有调用await()方法的线程都要排队。

countDown()方法

// java.util.concurrent.CountDownLatch.countDown()
public void countDown() 
    // 调用AQS的释放共享锁方法
    sync.releaseShared(1);

// java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseShared()
public final boolean releaseShared(int arg) 
    // 尝试释放共享锁,如果成功了,就唤醒排队的线程
    if (tryReleaseShared(arg)) 
        doReleaseShared();
        return true;
    
    return false;

countDown()方法,会释放一个共享锁,也就是count的次数会减1。

根据上面Sync的源码,我们知道,tryReleaseShared()每次会把count的次数减1,当其减为0的时候返回true,这时候才会唤醒等待的线程。

注意,doReleaseShared()是唤醒等待的线程,这个方法我们在前面的章节中分析过了。

使用案例

这里我们模拟一个使用场景,我们有一个主线程和5个辅助线程,等待主线程准备就绪了,5个辅助线程开始运行,等待5个辅助线程运行完毕了,主线程继续往下运行,大致的流程图如下:

技术图片

我们一起来看看这段代码应该怎么写:

public class CountDownLatchTest 
    public static void main(String[] args) throws InterruptedException 
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) 
            new Thread(()->
                try 
                    System.out.println("Aid thread is waiting for starting.");
                    startSignal.await();
                    // do sth
                    System.out.println("Aid thread is doing something.");
                    doneSignal.countDown();
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            ).start();
        

        // main thread do sth
        Thread.sleep(2000);
        System.out.println("Main thread is doing something.");
        startSignal.countDown();

        // main thread do sth else
        System.out.println("Main thread is waiting for aid threads finishing.");
        doneSignal.await();

        System.out.println("Main thread is doing something after all threads have finished.");

    

这段代码分成两段:

第一段,5个辅助线程等待开始的信号,信号由主线程发出,所以5个辅助线程调用startSignal.await()方法等待开始信号,当主线程的事儿干完了,调用startSignal.countDown()通知辅助线程开始干活。

第二段,主线程等待5个辅助线程完成的信号,信号由5个辅助线程发出,所以主线程调用doneSignal.await()方法等待完成信号,5个辅助线程干完自己的活儿的时候调用doneSignal.countDown()方法发出自己的完成的信号,当完成信号达到5个的时候,唤醒主线程继续执行后续的逻辑。

总结

(1)CountDownLatch表示允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作;

(2)CountDownLatch使用AQS的共享锁机制实现;

(3)CountDownLatch初始化的时候需要传入次数count;

(4)每次调用countDown()方法count的次数减1;

(5)每次调用await()方法的时候会尝试获取锁,这里的获取锁其实是检查AQS的state变量的值是否为0;

(6)当count的值(也就是state的值)减为0的时候会唤醒排队着的线程(这些线程调用await()进入队列);

彩蛋

(1)CountDownLatch的初始次数是否可以调整?

答:前面我们学习Semaphore的时候发现,它的许可次数是可以随时调整的,那么,CountDownLatch的初始次数能随时调整吗?答案是不能的,它没有提供修改(增加或减少)次数的方法,除非使用反射作弊。

(2)CountDownLatch为什么使用共享锁?

答:前面我们分析ReentrantReadWriteLock的时候学习过AQS的共享锁模式,比如当前锁是由一个线程获取为互斥锁,那么这时候所有需要获取共享锁的线程都要进入AQS队列中进行排队,当这个互斥锁释放的时候,会一个接着一个地唤醒这些连续的排队的等待获取共享锁的线程,注意,这里的用语是“一个接着一个地唤醒”,也就是说这些等待获取共享锁的线程不是一次性唤醒的。

说到这里,是不是很明白了?因为CountDownLatch的await()多个线程可以调用多次,当调用多次的时候这些线程都要进入AQS队列中排队,当count次数减为0的时候,它们都需要被唤醒,继续执行任务,如果使用互斥锁则不行,互斥锁在多个线程之间是互斥的,一次只能唤醒一个,不能保证当count减为0的时候这些调用了await()方法等待的线程都被唤醒。

(3)CountDownLatch与Thread.join()有何不同?

答:Thread.join()是在主线程中调用的,它只能等待被调用的线程结束了才会通知主线程,而CountDownLatch则不同,它的countDown()方法可以在线程执行的任意时刻调用,灵活性更大。

推荐阅读

1、 死磕 java同步系列之开篇

2、 死磕 java魔法类之Unsafe解析

3、 死磕 java同步系列之JMM(Java Memory Model)

4、 死磕 java同步系列之volatile解析

5、 死磕 java同步系列之synchronized解析

6、 死磕 java同步系列之自己动手写一个锁Lock

7、 死磕 java同步系列之AQS起篇

8、 死磕 java同步系列之ReentrantLock源码解析(一)——公平锁、非公平锁

9、 死磕 java同步系列之ReentrantLock源码解析(二)——条件锁

10、 死磕 java同步系列之ReentrantLock VS synchronized

11、 死磕 java同步系列之ReentrantReadWriteLock源码解析

12、 死磕 java同步系列之Semaphore源码解析

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

技术图片

死磕java同步系列之phaser源码解析

...?(2)Phaser具有哪些特性?(3)Phaser相对于CyclicBarrier和CountDownLatch的优势?简介Phaser,翻译为阶段,它适用于这样一种场景,一个大任务可以分为多个阶段完成,且每个阶段的任务可以多个线程并发执行,但是必须上一个阶段... 查看详情

死磕java同步系列之终结篇

欢×××”,查看更多源码系列文章,与彤哥一起畅游源码的海洋。(手机横屏看源码更方便)简介同步系列到此就结束了,本篇文章对同步系列做一个总结。脑图下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,... 查看详情

死磕java同步系列之synchronized解析(代码片段)

问题(1)synchronized的特性?(2)synchronized的实现原理?(3)synchronized是否可重入?(4)synchronized是否是公平锁?(5)synchronized的优化?(6)synchronized的五种使用方式?简介synchronized关键字是Java里面最基本的同步手段,它经... 查看详情

死磕java同步系列之semaphore源码解析(代码片段)

问题(1)Semaphore是什么?(2)Semaphore具有哪些特性?(3)Semaphore通常使用在什么场景中?(4)Semaphore的许可次数是否可以动态增减?(5)Semaphore如何实现限流?简介Semaphore,信号量,它保存了一系列的许可(permits),每次调... 查看详情

死磕java同步系列之semaphore源码解析(代码片段)

问题(1)Semaphore是什么?(2)Semaphore具有哪些特性?(3)Semaphore通常使用在什么场景中?(4)Semaphore的许可次数是否可以动态增减?(5)Semaphore如何实现限流?简介Semaphore,信号量,它保存了一系列的许可(permits),每次调... 查看详情

『死磕java并发编程系列』并发编程工具类之countdownlatch(代码片段)

《死磕Java并发编程》系列连载中,大家可以关注一波:👍🏻『死磕Java并发编程系列』01十张图告诉你多线程那些破事『死磕Java并发编程系列』02面试官:说说什么是Java内存模型?『死磕Java并发编程系列... 查看详情

『死磕java并发编程系列』并发编程工具类之countdownlatch(代码片段)

《死磕Java并发编程》系列连载中,大家可以关注一波:👍🏻『死磕Java并发编程系列』01十张图告诉你多线程那些破事『死磕Java并发编程系列』02面试官:说说什么是Java内存模型?『死磕Java并发编程系列... 查看详情

『死磕java并发编程系列』并发编程工具类之countdownlatch(代码片段)

《死磕Java并发编程》系列连载中,大家可以关注一波:👍🏻『死磕Java并发编程系列』01十张图告诉你多线程那些破事『死磕Java并发编程系列』02面试官:说说什么是Java内存模型?『死磕Java并发编程系列... 查看详情

死磕java同步系列之volatile解析(代码片段)

问题(1)volatile是如何保证可见性的?(2)volatile是如何禁止重排序的?(3)volatile的实现原理?(4)volatile的缺陷?简介volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不... 查看详情

死磕java同步系列之reentrantreadwritelock源码解析(代码片段)

问题(1)读写锁是什么?(2)读写锁具有哪些特性?(3)ReentrantReadWriteLock是怎么实现读写锁的?(4)如何使用ReentrantReadWriteLock实现高效安全的TreeMap?简介读写锁是一种特殊的锁,它把对共享资源的访问分为读访问和写访问... 查看详情

死磕java同步系列之jmm(javamemorymodel)(代码片段)

简介Java内存模型是在硬件内存模型上的更高层的抽象,它屏蔽了各种硬件和操作系统访问的差异性,保证了Java程序在各种平台下对内存的访问都能达到一致的效果。硬件内存模型在正式讲解Java的内存模型之前,我们有必要先了... 查看详情

死磕java同步系列之zookeeper分布式锁

问题(1)zookeeper如何实现分布式锁?(2)zookeeper分布式锁有哪些优点?(3)zookeeper分布式锁有哪些缺点?简介zooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它可以为分布式应用提供一致性的服务,它是Hadoop和... 查看详情

死磕java同步系列之mysql分布式锁

问题(1)什么是分布式锁?(2)为什么需要分布式锁?(3)mysql如何实现分布式锁?(4)mysql分布式锁的优点和缺点?简介随着并发量的不断增加,单机的服务迟早要向多节点或者微服务进化,这时候原来单机模式下使用的sync... 查看详情

死磕java同步系列之redis分布式锁进化史

问题(1)redis如何实现分布式锁?(2)redis分布式锁有哪些优点?(3)redis分布式锁有哪些缺点?(4)redis实现分布式锁有没有现成的轮子可以使用?简介Redis(全称:RemoteDictionaryServer远程字典服务)是一个开源的使用ANSIC语言... 查看详情

java并发之同步辅助类countdownlatch

CountDownLatch含义:CountDownLatch可以理解为一个计数器在初始化时设置初始值,当一个线程需要等待某些操作先完成时,需要调用await()方法。这个方法让线程进入休眠状态直到等待的所有线程都执行完成。每调用一次countDown()方法... 查看详情

死磕java线程系列之线程模型

问题(1)线程有哪些类型?(2)线程模型有哪些?(3)各语言是怎么实现自己的线程模型的?简介在Java中,我们平时所说的并发编程、多线程、共享资源等概念都是与线程相关的,这里所说的线程实际上应该叫作“用户线程... 查看详情

死磕java线程系列之线程池深入解析——未来任务执行流程

...怎么实现的呢?建议学习本章前先去看看彤哥之前写的《死磕java线程系列之自己动手写一个线程池 查看详情

java多线程系列:countdownlatch

这篇文章将介绍CountDownLatch这个同步工具类的基本信息以及通过案例来介绍如何使用这个工具。CountDownLatch是java.util.concurrent包下面的一个工具类,可以用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥... 查看详情