Java并发包源码学习系列:同步组件CountDownLatch源码解析
CountDownLatch概述
日常开发中,经常会遇到类似场景:主线程开启多个子线程执行任务,需要等待所有子线程执行完毕后再进行汇总。
在同步组件CountDownLatch出现之前,我们可以使用join方法来完成,简单实现如下:
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("A finish!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread B = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("B finish!");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("main thread wait ..");
A.start();
B.start();
A.join(); // 等待A执行结束
B.join(); // 等待B执行结束
System.out.println("all thread finish !");
}
}
但使用join方法并不是很灵活,并不能很好地满足某些场景的需要,而CountDown则能够很好地代替它,并且相比之下,提供了更多灵活的特性:
CountDownLatch相比join方法对线程同步有更灵活的控制,原因如下:
- 调用子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch使用计数器来允许子线程运行完毕或者运行中递减计数,await方法返回不一定必须等待线程结束。
- 使用线程池管理线程时,添加Runnable到线程池,没有办法再调用线程的join方法了。
使用案例与基本思路
public class TestCountDownLatch {
public static volatile CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main (String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("A finish!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
executorService.submit(() -> {
try {
Thread.sleep(1000);
System.out.println("B finish!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
System.out.println("main thread wait ..");
countDownLatch.await();
System.out.println("all thread finish !");
executorService.shutdown();
}
}
// 结果
main thread wait ..
B finish!
A finish!
all thread finish !
- 构建CountDownLatch实例,构造参数传参为2,内部计数初始值为2。
- 主线程构建线程池,提交两个任务,接着调用
countDownLatch.await()陷入阻塞。 - 子线程执行完毕之后调用
countDownLatch.countDown(),内部计数器减1。 - 所有子线程执行完毕之后,计数为0,此时主线程的await方法返回。
类图与基本结构

public class CountDownLatch {
/**
* Synchronization control For CountDownLatch.
* Uses AQS state to represent count.
*/
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
//...
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
CountDownLatch基于AQS实现,内部维护一个Sync变量,继承了AQS。
在AQS中,最重要的就是state状态的表示,在CountDownLatch中使用state表示计数器的值,在初始化的时候,为state赋值。
几个同步方法实现比较简单,如果你不熟悉AQS,推荐你瞅一眼前置文章:
- Java并发包源码学习系列:AbstractQueuedSynchronizer
- Java并发包源码学习系列:CLH同步队列及同步资源获取与释放
- Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别
- Java并发包源码学习系列:详解Condition条件队列、signal和await
- Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类
接下来我们简单看一看实现,主要学习两个方法:await()和countdown()。
void await()
当线程调用CountDownLatch的await方法后,线程会被阻塞,除非发生下面两种情况:
- 内部计数器值为0,
getState() == 0。 - 被其他线程中断,抛出异常,也就是
currThread.interrupt()。
// CountDownLatch.java
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS.java
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程中断, 则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 由子类实现,这里再Sync中实现,计数器为0就可以返回,否则进入AQS队列等待
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// Sync
// 计数器为0 返回1, 否则返回-1
private static final class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
}
boolean await(long timeout, TimeUnit unit)
当线程调用CountDownLatch的await方法后,线程会被阻塞,除非发生下面三种情况:
- 内部计数器值为0,
getState() == 0,返回true。 - 被其他线程中断,抛出异常,也就是
currThread.interrupt()。 - 设置的timeout时间到了,超时返回false。
// CountDownLatch.java
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// AQS.java
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout);
}
void countDown()
调用该方法,内部计数值减1,递减后如果计数器值为0,唤醒所有因调用await方法而被阻塞的线程,否则跳过。
// CountDownLatch.java
public void countDown() {
sync.releaseShared(1);
}
// AQS.java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync
private static final class Sync extends AbstractQueuedSynchronizer {
protected boolean tryReleaseShared(int releases) {
// 循环进行CAS操作
for (;;) {
int c = getState();
// 一旦为0,就返回false
if (c == 0)
return false;
int nextc = c-1;
// CAS尝试将state-1,只有这一步CAS成功且将state变成0的线程才会返回true
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
总结
CountDownLatch相比于join方法更加灵活且方便地实现线程间同步,体现在以下几点:
- 调用子线程的join()方法后,该线程会一直被阻塞直到子线程运行完毕,而CountDownLatch使用计数器来允许子线程运行完毕或者运行中递减计数,await方法返回不一定必须等待线程结束。
- 使用线程池管理线程时,添加Runnable到线程池,没有办法再调用线程的join方法了。
CountDownLatch使用state表示内部计数器的值,初始化传入count。
线程调用countdown方法将会原子性地递减AQS的state值,线程调用await方法后将会置入AQS阻塞队列中,直到计数器为0,或被打断,或超时等才会返回,计数器为0时,当前线程还需要唤醒由于await()被阻塞的线程。
参考阅读
- 《Java并发编程之美》
Java并发包源码学习系列:同步组件CountDownLatch源码解析的更多相关文章
- Java并发包源码学习系列:CLH同步队列及同步资源获取与释放
目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...
- Java并发包源码学习系列:同步组件CyclicBarrier源码解析
目录 CyclicBarrier概述 案例学习 类图结构及重要字段 内部类Generation及相关方法 void reset() void breakBarrier() void nextGener ...
- Java并发包源码学习系列:同步组件Semaphore源码解析
目录 Semaphore概述及案例学习 类图结构及重要字段 void acquire() 非公平 公平策略 void acquire(int permits) void acquireUninterr ...
- Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别
目录 Java并发包源码学习系列:AQS共享模式获取与释放资源 独占式获取资源 void acquire(int arg) boolean acquireQueued(Node, int) 独占式释放 ...
- Java并发包源码学习系列:ReentrantLock可重入独占锁详解
目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...
- Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析
目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...
- Java并发包源码学习系列:详解Condition条件队列、signal和await
目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...
- Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类
目录 LockSupport概述 park与unpark相关方法 中断演示 blocker的作用 测试无blocker 测试带blocker JDK提供的demo 总结 参考阅读 系列传送门: Jav ...
- Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析
目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...
随机推荐
- 图文并茂-超详解 CS:APP: Lab3-Attack(附带栈帧分析)
CS:APP:Lab3-ATTACK 0. 环境要求 关于环境已经在lab1里配置过了.lab1的连接如下 实验的下载地址如下 说明文档如下 http://csapp.cs.cmu.edu/3e/at ...
- spring MVC 3.2中@ResponseBody(Post接口)返回乱码的完美解决方案
本来因为ajax跨域http远程调用时有问题,在服务端响应时用以下方式解决了,但IE8及下有问题. response.addHeader("Access-Control-Allow-Orig ...
- Java 复习整理day05
1 package com.it.demo01_oop; 2 3 import java.util.Arrays; 4 5 /* 6 案例: 演示面向过程和面向对象代码的区别 7 8 面向过程编程思想 ...
- (史上最全)SNP位点与转录因子结合特异性数据库:GVATdb
众所周知,全基因组关联分析(GWAS)发现的很多变异位点基本为非编码,这些变异位点1)要么调控基因表达(eQTL); 2)要么影响增强子活性; 3)要么影响转录因子(TF)结合特异性; 4)要么啥也不 ...
- 2019牛客暑期多校训练营(第四场)D-triples I
>传送门< 题意:求最少需要多少个3的倍数按位或后可以得到数字a 思路:利用3的倍数对应的二进制数的性质来先选出一个x,然后根据数字a再配一个y出来 首先,我们都知道十进制中,任意一个数只 ...
- Chocolate Bunny CodeForces - 1407C 思维
题意: 交互题 题目输入一个n,你需要输出一个满足要求的[1,n]的排列. 你可以最多询问2*n次来确定你要输出的排列·中每一个位置的值 每一次询问格式为"? a b" 它会回复你 ...
- Educational Codeforces Round 94 (Rated for Div. 2) String Similarity、RPG Protagonist、Binary String Reconstruction、Zigzags 思维
题目链接:String Similarity 题意: 首先题目定义了两个串的相似(串的构成是0.1),如果两个串存在对于一个下标k,它们的值一样,那么这两个串就相似 然后题目给你一个长度为2n-1的串 ...
- Codeforces Round #547 (Div. 3) D. Colored Boots (贪心,模拟)
题意:有两个字符串,两个字符串中的相同字符可以相互匹配,\(?\)可以和任意字符匹配,输出最大匹配的字符数量和它们分别两个字符串中的位置. 题解:很容易贪心,我们先遍历第一个字符串,然后在第二个字符串 ...
- java中的装箱及拆箱
java中存在8中基本的数据类型,每一种数据类型都有包装类型. 包装类型:每一个基本的数据类型都会------对应一个包装类型. boolean------------------>Boolea ...
- Leetcode(11)-盛最多水的容器
给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) .画 n 条垂直线,使得垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0).找出其中的两条线,使得 ...