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

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

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


问题

(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源码解析

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

原文地址:https://www.cnblogs.com/tong-yuan/p/CountDownLatch.html

时间: 2024-12-15 11:54:41

死磕 java同步系列之CountDownLatch源码解析的相关文章

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

问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWriteLock的对比? 简介 StampedLock是java8中新增的类,它是一个更加高效的读写锁的实现,而且它不是基于AQS来实现的,它的内部自成一片逻辑,让我们一起来学习吧. StampedLock具有三种模式:写模式.读模式.乐观读模式. ReentrantReadWriteLock中的读和写都是

死磕 java同步系列之CyclicBarrier源码解析——有图有真相

问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier,回环栅栏,它会阻塞一组线程直到这些线程同时达到某个条件才继续执行.它与CountDownLatch很类似,但又不同,CountDownLatch需要调用countDown()方法触发事件,而CyclicBarrier不需要,它就像一个栅栏一样,当一组线程都到达了栅栏处才继续往下走. 使用方法 pu

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

问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这样一种场景,一个大任务可以分为多个阶段完成,且每个阶段的任务可以多个线程并发执行,但是必须上一个阶段的任务都完成了才可以执行下一个阶段的任务. 这种场景虽然使用CyclicBarrier或者CountryDownLatch也可以实现,但是要复杂的多.首先,具体需要多少个阶段是可能会变的,其次,每个阶

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

问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的TreeMap? 简介 读写锁是一种特殊的锁,它把对共享资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,但是同一时间只能有一个线程对共享资源进行写访问,使用读写锁可以极大地提高并发量. 特性 读写锁具有以下特性: 是否互斥 读 写 读 否 是 写 是 是 可以看到,读写锁除了读读

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

问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaphore如何实现限流? 简介 Semaphore,信号量,它保存了一系列的许可(permits),每次调用acquire()都将消耗一个许可,每次调用release()都将归还一个许可. 特性 Semaphore通常用于限制同一时间对共享资源的访问次数上,也就是常说的限流. 下面我们一起来学习Java

死磕 java同步系列之AQS终篇(面试)

问题 (1)AQS的定位? (2)AQS的重要组成部分? (3)AQS运用的设计模式? (4)AQS的总体流程? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为Java中几乎所有的锁和同步器提供一个基础框架. 在之前的章节中,我们一起学习了ReentrantLock.ReentrantReadWriteLock.Semaphore.CountDownLatch的源码,今天我们一起来对AQS做个总结. 状态变量state AQS中定义了一个状态变量state

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

问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它可以为分布式应用提供一致性服务,它是Hadoop和Hbase的重要组件,同时也可以作为配置中心.注册中心运用在微服务体系中. 本章我们将介绍zookeeper如何实现分布式锁运用在分布式系统中. 基础知识 什么是znode? zooKeeper操作和维护的为一个个数据节点,称为 z

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

问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API. 本章我们将介绍如何基于redis实现分布式锁,并把其实现的进化史从头到尾讲明白,以便大家在面试的时候能讲清楚

死磕 java同步系列之终结篇

简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. 如果有需要高清无码原图的同学,可以关注公众号"彤哥读源码",回复"sync"领取. 总结 所谓同步,就是保证多线程(包括多进程)对共享资源的读写能够安全有效的运行. 根据同步的运用场景的不同,实现同步的方式也是随之一起变化,但是总结下来,这些实现方式之间又有一些共通之