简介

CountDownLatch是JUC提供的一个线程同步工具,主要功能就是协调多个线程之间的同步,或者说实现线程之间的通信

CountDown,数数字,只能往下数。Latch,门闩。光看名字就能明白这个CountDownLatch是如何使用的了哈哈。CountDownLatch就相当于一个计数器,计数器的初值通过构造方法的参数来设置。调用CountDownLatch实例的await方法的线程,会等待计数器变为0才会被唤醒,继续向下执行。那么计数器如何变为0呢?

如果其他线程调用该CountDownLatch实例的countDown方法,会将计数值减1。当减为0时,会让那些因调用await方法而阻塞等待的线程继续执行。这样就实现了这些线程之间的同步功能

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15730573.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

使用场景

直接介绍或许太抽象,对于初学者来说很难理解,最好的方式就是通过一个实际场景来引入

有一种通用的场景:主线程开启多个子线程去并行执行多个子任务,等待所有子线程执行完毕,主线程收集子线程的执行结果并统计

场景示例

比如,主线程等A和B给它转账,等收齐所有钱再一并放入银行赚利息。示例代码如下:

public class TestCountDownLatch {

    // 这里必须是原子类,要保证对money的修改是原子性操作,才能保证线程安全
// 仅把money设置为volatile int是不行的哦
private static final AtomicInteger money = new AtomicInteger(0); public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2); Thread threadA = new Thread(() -> {
try {
System.out.println("A转账30元给我");
Thread.sleep(1000); // 线程A和线程B对money必须要使用CAS修改,否则可能会出错
int origin = money.get();
while (!money.compareAndSet(origin, origin + 30)) {
origin = money.get();
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("A转账完成");
}); Thread threadB = new Thread(() -> {
try {
System.out.println("B转账70元给我");
Thread.sleep(1000);
int origin = money.get();
while (!money.compareAndSet(origin, origin + 30)) {
origin = money.get();
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("B转账完成");
}); System.out.println("等A和B转账给我...");
threadA.start();
threadB.start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(latch.getCount());
System.out.println("转账完成,将钱存入银行..."); // 这里就不用CAS修改money,因为只有main线程对money做修改
int origin = money.get();
money.set(origin * 2);
System.out.println("将钱去除,本金加利润一共" + money.get() + "元");
}
}

命令行输出如下:

点击查看代码
等A和B转账给我...
A转账30元给我
B转账70元给我
A转账完成
B转账完成
转账完成,将钱存入银行...
将钱去除,本金加利润一共120元 Process finished with exit code 0

得益于CountDownLatch的同步功能,当上述代码执行结束时,money的值必定是120,而不会是0、60或140。因为主线程会一致await直到线程A和线程B都执行完latch.countDown()才会继续往下执行

实现原理

CountDownLatch的实现原理其实就是AbstractQueuedSynchronizer(AQS)

CountDownLatch有一个内部类Sync,它实现了AQS类定义的部分钩子方法,CountDownLatch通过Sync类实例sync实现了所有功能,调用CountDownLatch的方法都会委托给sync域来执行

// 所有CountDown的功能都是委托给这个Sync类对象来完成
private final Sync sync;

因此,要搞懂CountDownLatch,必须搞懂AQS以及Sync类。接下来就跟我一起来剖析一下源码,看看这个Sync到底干了些啥

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15730573.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

源码剖析

构造方法

CountDownLatch的构造函数中可以传入count参数,表明必须调用countcountDown方法,才能让调用await的线程继续向下执行。其源码如下:

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

实际上是初始化了一个Sync类对象,并注入到CountDownLatchsync域中。Sync构造方法如下:

Sync(int count) {
setState(count);
}

Sync构造方法实际上就是设置了AQS中的state,将其设置为初始计数值为count

重要结论:AQS的state就表示CountDownLatch当前的计数值

await

await方法是一个实例方法,调用它的线程一直阻塞等待,直到CountDownLatch对象的计数值降为0,才能被唤醒。如果调用await时计数值就已经是0,就不会被阻塞

await方法是响应中断的:

  • 如果一个线程在调用await方法之前就已经被中断,那么调用时会直接抛出中断异常
  • 如果一个线程调用await方法阻塞等待过程中,收到中断信号,就会抛出中断异常

说了这么多,还是来看看await的源码吧:

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

可以看到,这个方法实际上是委托给了Sync类对象sync来执行,这里的acquireSharedInterruptibly已经由Sync类的父类AQS提供了实现,源码如下:

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

这段代码在 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析 系列中有详细介绍,不过那里并没有涉及到具体的应用类(如CountDownLatch这种),只是高屋建瓴地分析过,这里正好借助CountDownLatch来更好地理解它

acquireSharedInterruptibly源码中可以看到,如果线程在调用await之前就已经被设置了中断状态,那么会直接抛出InterruptedException异常

该方法接下来会调用钩子方法tryAcquireSharedSync类为该方法提供了具体实现,源码如下:

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

AQS虽然没有为tryAcquireShared提供具体实现,但是规定了返回值的含义:

  • 负数:表明获取失败,该线程需要被加入同步队列阻塞等待
  • 0:表明获取共享资源成功,但是后续获取共享资源一定不会成功
  • 正数:表明获取共享资源成功,而且后续的获取也可能成功

让我们来分析一下Sync类实现的tryAcquireShared方法:

  • 如果state不为0,即CountDownLatch对象的计数值还没减到0,则返回-1,会继续执行doAcquireSharedInterruptibly方法,将调用await的线程加入同步队列阻塞等待
  • 如果state为0,即CountDownLatch对象的计数值已经减为0,则返回1,调用await会直接返回,不会被阻塞

注:doAcquireSharedInterruptibly的具体分析见 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析 系列

countDown

countDown方法也是实例方法,调用它会将CountDownLatch对象的计数值减1。如果正好减为0,那么会将所有因调用await而被阻塞的线程都唤醒。其源码如下:

public void countDown() {
sync.releaseShared(1);
}

该方法实际上委托给sync来执行,这里的releaseShared方法在Sync类的父类AQS中提供了具体实现,其源码如下:

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

该方法首先会调用钩子方法tryReleaseShared,该方法在Sync类中提供了具体实现,源码如下:

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;
}
}

AQS虽然没有为tryReleaseShared提供具体实现,但是规定了返回值的含义:

  • true:此次释放资源的行为可能会让一个阻塞等待中的线程被唤醒
  • false:otherwise

让我们来分析一下Sync类实现的tryReleaseShared方法:

该方法所有代码都包含在一个for循环中,这是为了应对CAS失败的情况。循环体内CAS修改state,即将CountDownLatch的计数值减1

如果CountDownLatch的计数值减1后变成0,则返回true。那么releaseShared方法会继续调用doReleaseShared方法,唤醒同步队列中的后续线程

如果不为0,则返回false,无事发生~

注:doReleaseShared方法的作用是唤醒队首线程,并确保状态传播,该方法的详细解释见 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析 系列

getCount

getCount方法就是返回CountDownLatch对象当前的计数值,源码如下:

public long getCount() {
return sync.getCount();
}

实际上委托给了sync对象的getCount方法来执行,其源码如下:

int getCount() {
return getState();
}

其实就是调用AQS的getState方法,返回当前的state,即CountDownLatch的计数值,很简单哦~

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15730573.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

CountDownLatch与join方法的区别

当然,在上述场景中,也可以使用Thread对象方法join来实现这一点,在主线程中调用所有子线程的join方法,再执行结果收集和统计任务。上面例子如果使用join方法来实现,代码如下:

public class TestJoin {
private static final AtomicInteger money = new AtomicInteger(0); public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try {
System.out.println("A转账30元给我");
Thread.sleep(1000); int origin = money.get();
while (!money.compareAndSet(origin, origin + 30)) {
origin = money.get();
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A转账完成");
}); Thread threadB = new Thread(() -> {
try {
System.out.println("B转账70元给我");
Thread.sleep(1000);
int origin = money.get();
while (!money.compareAndSet(origin, origin + 30)) {
origin = money.get();
continue;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B转账完成");
}); System.out.println("等A和B转账给我...");
threadA.start();
threadB.start();
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("转账完成,将钱存入银行..."); int origin = money.get();
money.set(origin * 2);
System.out.println("将钱去除,本金加利润一共" + money.get() + "元");
}
}

命令行输出如下:

点击查看代码
等A和B转账给我...
A转账30元给我
B转账70元给我
B转账完成
A转账完成
转账完成,将钱存入银行...
将钱去除,本金加利润一共120元 Process finished with exit code 0

但是,和CountDownLatch借助AQS不同,join方法的执行原理是:不停地检查调用线程是否执行完毕。如果没有,则让当前线程wait。否则才会调用notifyAll将当前线程唤醒

从执行原理上就可以看出它们的区别主要在于两点

  • join方法没有CountDownLatch灵活:使用join方法必须等待调用线程执行完毕,后面就不能再继续执行了。而CountDownLatchcountDown方法可以放在调用线程的run方法中间,这样调用线程不必执行结束,就能唤醒其他await的线程
  • 调用join方法的线程会一直消耗CPU资源,不会阻塞挂起,即“忙等”,而调用了CountDownLatchawait方法的线程会被阻塞挂起,让出CPU执行权,只有等条件合适并被线程调度后才能占用CPU资源

CountDownLatch源码阅读的更多相关文章

  1. 《java.util.concurrent 包源码阅读》 结束语

    <java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...

  2. Java - "JUC" CountDownLatch源码分析

    Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例 CountDownLatch简介 CountDownLatch是一个同步辅助类,在完成一组正在其他线程中执行的操作之前 ...

  3. TBSchedule源码阅读1-TBScheduleManagerFactory

    TBSchedule 1 TBScheduleManagerFactory 初始化    成员变量    ZKManager;    IScheduleDataManager;    Schedule ...

  4. ZooKeeper源码阅读——client(二)

    原创技术文章,转载请注明:转自http://newliferen.github.io/ 如何连接ZooKeeper集群   要想了解ZooKeeper客户端实现原理,首先需要关注一下客户端的使用方式, ...

  5. 【原】FMDB源码阅读(三)

    [原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...

  6. 【原】FMDB源码阅读(二)

    [原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...

  7. 【原】FMDB源码阅读(一)

    [原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ...

  8. 【原】AFNetworking源码阅读(六)

    [原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...

  9. 【原】AFNetworking源码阅读(五)

    [原]AFNetworking源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中提及到了Multipart Request的构建方法- [AFHTTP ...

随机推荐

  1. Codeforces 1423N - BubbleSquare Tokens(归纳+构造)

    Codeforces 题目传送门 & 洛谷题目传送门 一道思维题. 题目没有说无解输出 \(-1\),这意味着对于任意 \(G\) 一定存在一个合法的排列方案.因此可以考虑采用归纳法.对于一个 ...

  2. 【R绘图】R 基础(base )低级函数legend绘图?

    ggplot虽然好用,但base才是真正的瑞士军刀,什么都能用,各种自定义图形自由组合,出版级图片用base才是王道.但要达到随心所欲,需要熟练掌握. legend是比较重要的低级函数,有很多细节处理 ...

  3. No.1 R语言在生物信息中的应用——序列读取及格式化输出

    目的:读入序列文件(fasta格式),返回一个数据框,内容包括--存储ID.注释行(anno).长度(len).序列内容(content) 一.问题思考: 1. 如何识别注释行和序列内容行 2. 如何 ...

  4. APP工程师接入Telink Mesh流程 -3

    加密是为了使网络更加的安全.健壮,若由于login.加密等流程 严重影响了 开发进程,也可以通过 修改SDK 固件 将login.加密 环节取消 1.发送数据.接受数据加密,解密去掉 mesh_sec ...

  5. 【玩具】获取B站视频的音频片段

    事情是这样的,我有个和社畜的社会地位不太相符的小爱好--听音乐剧. 基本上是在B站上点开视频听,不是不想在网易云或者QQ音乐听,只是在这些音乐软件上面,我想听的片段要不就收费,要不版本不是我喜欢的,要 ...

  6. CSS3实现字体描边

    CSS3实现字体描边的两种方法 -webkit-text-stroke: 1px #fff;:不建议,向内描边,字体颜色变细,效果不佳: 用box-shadow模拟描边,向外描边,保留字体粗细,赞! ...

  7. 9 — springboot整合jdbc、druid、druid实现日志监控 — 更新完毕

    1.整合jdbc.druid 1).导入依赖 <dependency> <groupId>org.springframework.boot</groupId> &l ...

  8. .NET Core基础篇之:集成Swagger文档与自定义Swagger UI

    Swagger大家都不陌生,Swagger (OpenAPI) 是一个与编程语言无关的接口规范,用于描述项目中的 REST API.它的出现主要是节约了开发人员编写接口文档的时间,可以根据项目中的注释 ...

  9. LeetCode移除元素

    LeetCode 移除元素 题目描述 给你一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,并返回移除后数组的新长度. 不需要使用额外的数组空间,你必须仅使用 O(1) ...

  10. 学习java 7.5

    学习内容: Alt + Insert 快捷键 根据需要选择操作 继承的格式 public class 子类名 extends 父类名{} 继承好处:提高了代码的复用性,维护性 弊端:改变父类,子类也改 ...