CountDownLatch与CyclicBarrier的基本使用
1 概述
CountDownLatch以及CyclicBarrier都是Java里面的同步工具之一,本文介绍了两者的基本原理以及基本使用方法。
2 CountDownLatch
CountDownLatch是一个同步工具类,常见的使用场景包括:
- 允许一个或多个线程等待一系列的其他线程结束
- 在串行化任务中需要进行并行化处理,并等待所有并行化任务结束,串行化任务才能继续进行
比如考虑这样一个场景,在一个电商网站中,用户点击了首页,需要一部分的商品,同时显示它们的价格,那么,调用的流程应该是:
- 获取商品
- 计算售价
- 返回所有商品的最终售价
解决这样的问题可以使用串行化或并行化操作,串行化就是逐一计算商品的售价,并返回,并行化就是获取商品后,并行计算每一个商品的售价,最后返回,显然后一种方案要比前一种要好,那么这时候就可以用上CountDownLatch了。
一份简单的模拟代码如下:
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.concurrent.ThreadLocalRandom.current;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException{
List<Price> list = IntStream.rangeClosed(1,10).mapToObj(Price::new).collect(Collectors.toList());
//计数器大小为商品列表的长度
final CountDownLatch latch = new CountDownLatch(list.size());
//线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
list.forEach(p-> executor.execute(()->{
System.out.println("Product "+p.id+" start calculate price ");
try{
//随机休眠模拟业务操作耗时
TimeUnit.SECONDS.sleep(current().nextInt(10));
p.setPrice(p.getPrice()*((p.getId() & 1) == 1 ? 0.9 : 0.7));
System.out.println("Product "+p.id+" calculate price completed");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
//每完成计算一个商品,将计数器减1,注意需要放在finally中
latch.countDown();
}
}));
//主线程阻塞直到所有的计数器为0,也就是等待所有的子任务计算价格完毕
latch.await();
System.out.println("All of prices calculate finished");
//手动终止,不然不会结束运行
executor.shutdown();
}
private static class Price{
private final int id;
private double price;
public Price(int id) {
this.id = id;
}
public int getId() {
return id;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
}
输出:

代码比较简单,关键地方用上了注释,可以看到代码执行顺序如下:
- 创建多个任务计算商品的价格
- 主线程阻塞
- 计算完成后,将计数器减1
- 当计数器为0时,主线程退出阻塞状态
值得注意的是计数器减1的操作需要放在finally中,因为有可能会出现异常,如果出现异常导致计数器不能减少,那么主线程会一直阻塞。
另外,CountDownLatch还有一个await(long timeout,TimeUnit unit)方法,是带有超时参数的,也就是说,如果在超时时间内,计数器的值还是大于0(还有任务没执行完成),会使得当前线程退出阻塞状态。
3 CyclicBarrier
CyclicBarrier与CountDownLatch有很多类似的地方,也是一个同步工具类,允许多个线程在执行完相应的操作之后彼此等待到达同一个barrier point(屏障点)。CyclicBarrier也适合某个串行化的任务被拆分为多个并行化任务,这点与CountDownLatch类似,但是CyclicBarrier具备的一个更强大的功能是,CyclicBarrier可以被重复使用。
3.1 等待完成
先简单说一下CyclicBarrier的实现原理:
- 初始化
CyclicBarrier,传入一个int参数,表示分片(parites),通常意义上来说分片数就是任务的数量 - 同时串行化执行多个任务
- 任务执行完成后,调用
await(),等待其他线程也到达barrier point - 当所有线程到达后,继续以串行化方式运行任务
常见的使用方法是设置分片数为任务数+1,这样,可以在主线程中执行await(),等待所有子任务完成。比如下面是使用CyclicBarrier实现同样功能的模拟代码:
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.concurrent.ThreadLocalRandom.current;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException,BrokenBarrierException{
List<Price> list = IntStream.rangeClosed(1,10).mapToObj(Price::new).collect(Collectors.toList());
final CyclicBarrier barrier = new CyclicBarrier(11);
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
list.forEach(p-> executor.execute(()->{
System.out.println("Product "+p.id+" start calculate price ");
try{
TimeUnit.SECONDS.sleep(current().nextInt(10));
p.setPrice(p.getPrice()*((p.getId() & 1) == 1 ? 0.9 : 0.7));
System.out.println("Product "+p.id+" calculate price completed");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
try{
barrier.await();
}catch (InterruptedException | BrokenBarrierException e){
e.printStackTrace();
}
}
}));
barrier.await();
System.out.println("All of prices calculate finished");
executor.shutdown();
}
private static class Price{
private final int id;
private double price;
public Price(int id) {
this.id = id;
}
public int getId() {
return id;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
}
输出相同,代码大部分相似,不同的地方有:
latch.countDown()替换成了barrier.await()latch.await()替换成了barrier.await()- 线程池的核心线程数替换成了
10
await()方法会等待所有的线程到达barrier point,上面代码执行流程简述如下:
- 初始化
CyclicBarrier,分片数为11(子线程数+1) - 主线程调用
await(),等待子线程执行完成 - 子线程各自进行商品价格的计算,计算完成后,调用
await(),等待其他线程也到达barrier point - 当所有子线程计算完成后,由于没有后续操作,所以子线程运行结束,同时由于主线程还有后续操作,会先输出提示信息再终止线程池
注意一个很大的不同就是这里的线程池核心线程数目改成了 10,那么,为什么需要10?
因为如果是设置一个小于10的核心线程个数,由于线程池是会先创建核心线程来执行任务,核心线程满了之后,放进任务队列中,而假设只有5个核心线程,那么:
- 5个线程进行计算价格
- 另外5个任务放在任务队列中
这样的话,会出现死锁,因为计算中的线程需要队列中的任务到达barrier point才能结束,而队列中的任务需要核心线程计算完毕后,才能调度出来计算,这样死锁就出现了。
3.2 重复使用
CyclicBarrier与CountDownLatch的一个最大不同是,CyclicBarrier可以被重复使用,原理上来说,await()会将内部计数器减1,当计数器减为0时,会自动进行计数器(分片数)重置。比如,在上面的代码中,由于遇上促销活动,需要对商品的价格再次进行计算:
import java.util.List;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static java.util.concurrent.ThreadLocalRandom.current;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException,BrokenBarrierException{
List<Price> list = IntStream.rangeClosed(1,10).mapToObj(Price::new).collect(Collectors.toList());
final CyclicBarrier barrier = new CyclicBarrier(11);
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,2, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
list.forEach(p-> executor.execute(()->{
System.out.println("Product "+p.id+" start calculate price.");
try{
TimeUnit.SECONDS.sleep(current().nextInt(10));
p.setPrice(p.getPrice()*((p.getId() & 1) == 1 ? 0.9 : 0.7));
System.out.println("Product "+p.id+" calculate price completed.");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
try{
barrier.await();
}catch (InterruptedException | BrokenBarrierException e){
e.printStackTrace();
}
}
}));
barrier.await();
System.out.println("All of prices calculate finished.");
//复制的一段相同代码
list.forEach(p-> executor.execute(()->{
System.out.println("Product "+p.id+" start calculate price again.");
try{
TimeUnit.SECONDS.sleep(current().nextInt(10));
p.setPrice(p.getPrice()*((p.getId() & 1) == 1 ? 0.9 : 0.7));
System.out.println("Product "+p.id+" calculate price completed.");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
try{
barrier.await();
}catch (InterruptedException | BrokenBarrierException e){
e.printStackTrace();
}
}
}));
barrier.await();
System.out.println("All of prices calculate finished again.");
executor.shutdown();
}
private static class Price{
private final int id;
private double price;
public Price(int id) {
this.id = id;
}
public int getId() {
return id;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
}
将计算价格的代码复制一遍,其中没有手动修改计数器,只是调用await(),输出如下:

可以看到,并没有对CycliBarrier进行类似reset之类的操作,但是依然能按正常逻辑运行,这是因为await()内部会维护一个计数器,当计数器为0的时候,会自动进行重置,下面是await()在OpenJDK 11下的源码:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return this.dowait(false, 0L);
} catch (TimeoutException var2) {
throw new Error(var2);
}
}
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
ReentrantLock lock = this.lock;
lock.lock();
byte var9;
try {
//...
int index = --this.count;
if (index != 0) {
//计数器不为0的情况
//....
}
boolean ranAction = false;
try {
Runnable command = this.barrierCommand;
if (command != null) {
command.run();
}
ranAction = true;
this.nextGeneration();
var9 = 0;
} finally {
if (!ranAction) {
this.breakBarrier();
}
}
} finally {
lock.unlock();
}
return var9;
}
private void nextGeneration() {
this.trip.signalAll();
this.count = this.parties;
this.generation = new CyclicBarrier.Generation();
}
当计数器为0时,会生成新的Generation,并将var9置为0,最后返回var9(在这个方法中var9只有一处赋值,就是代码中的var9=0,可以理解成直接返回0)。
3.3 CyclicBarrier其他的一些常用方法
CyclicBarrier(int parties,Runnable barrierAction):构造的时候传入一个Runnable,表示所有线程到达barrier point时,会调用该Runnableawait(long timeout,TimeUnit unit):与无参的await()类似,底层调用的是相同的doWait(),不过增加了超时功能isBroken():返回broken状态,某个线程由于执行await而进入阻塞,此时如果执行了中断操作(比如interrupt),那么isBroken()会返回true。需要注意,处于broken状态的CyclicBarrier不能被直接使用,需要调用reset()进行重置
4 总结
下面是CountDownLatch与CyclicBarrier的一些简单比较,相同点如下:
- 都是
java.util.concurrent包下的线程同步工具类 - 都可以用于“主线程阻塞一直等待,直到子任务完成,主线程才继续执行”的情况
不同点:
CountDownLatch的await()方法会等待计数器归0,而CyclicBarrier的await()会等待其他线程到达barrier pointCyclicBarrier内部的计数器是可以被重置的,但是CountDownLatch不可以CyclicBarrier是由Lock和Condition实现的,而CountDownLatch是由同步控制器AQS实现的- 构造时
CyclicBarrier不允许parties为0,而CountDownLatch允许count为0
如果觉得文章好看,欢迎点赞。
同时欢迎关注微信公众号:氷泠之路。

CountDownLatch与CyclicBarrier的基本使用的更多相关文章
- Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
Java并发编程:CountDownLatch.CyclicBarrier和Semaphore 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch ...
- 并发工具类:CountDownLatch、CyclicBarrier、Semaphore
在多线程的场景下,有些并发流程需要人为来控制,在JDK的并发包里提供了几个并发工具类:CountDownLatch.CyclicBarrier.Semaphore. 一.CountDownLatch ...
- Java并发(8):CountDownLatch、CyclicBarrier、Semaphore、Callable、Future
CountDownLatch.CyclicBarrier.Semaphore.Callable.Future 都位于java.util.concurrent包下,其中CountDownLatch.C ...
- 【Java多线程】JUC包下的工具类CountDownLatch、CyclicBarrier和Semaphore
前言 JUC中为了满足在并发编程中不同的需求,提供了几个工具类供我们使用,分别是CountDownLatch.CyclicBarrier和Semaphore,其原理都是使用了AQS来实现,下面分别进行 ...
- CountDownLatch,CyclicBarrier,Semaphore
CountDownLatch是倒数,doneSignal = new CountDownLatch(LATCH_SIZE);赋初值后,在主线程中等待doneSignal.await();其它线程中,每 ...
- CountDownLatch、CyclicBarrier、Semaphore、Exchanger
CountDownLatch: 允许N个线程等待其他线程完成执行.无法进行重复使用,只能用一次. 比如有2个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch ...
- CountDownLatch、CyclicBarrier和Semaphore
转载:http://www.cnblogs.com/dolphin0520/p/3920397.html 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDown ...
- CountDownLatch和CyclicBarrier的区别
[CountDownLatch.CyclicBarrier和Semaphore]http://www.cnblogs.com/dolphin0520/p/3920397.html [CountDo ...
- 使用Java辅助类(CountDownLatch、CyclicBarrier、Semaphore)并发编程
在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法 一.C ...
- CountDownLatch 和 CyclicBarrier 的运用及实现原理
I.CountDownLatch 和 CyclicBarrier 的运用 CountDownlatch: 定义: 其是一个线程同步的辅助工具,通过它可以做到使一条线程一直阻塞等待,直到其他线程完成其所 ...
随机推荐
- 遇见ZooKeeper:初识
0. 什么是ZooKeeper ZooKeeper 是一个开源的分布式,它的设计目标是将那些复杂且容易出错的分布式协同服务封装起来,抽象出一个高效可靠的原语集,并以一系列简单的接口提供个用户使用. Z ...
- mybatis 一对多和多对一 简单案例笔记
以案例说明(以下案例代码都敲过验证过) 多对一(多个学生对一个老师 即学生集合中都存一个老师对象) Mybatis多对一实现方式1: //定义Student 和 Teacher 实体 @Data p ...
- c#winform主题实现的一个方法
winform的主题实现没有bs里面那么舒服,下面做了一个简单实现,记录一下. 1.一个接口,需要做主题的控件.窗体都要实现这个接口 /// <summary> /// 使用主题的控件.窗 ...
- Filter理解
web中Filter通过<init-param>添加参数.web.xml中的配置: <filter> <filter-name>AuthFilter</fil ...
- 代码小知识之UUID
1.生成UUID(UUID保证对在同一时空中的所有机器都是唯一的,UUID的唯一缺陷在于生成的结果串会比较长.UUID 来作为数据库数据表主键是非常不错的选择,保证每次生成的UUID 是唯一的) UU ...
- Dyno-queues 分布式延迟队列 之 辅助功能
Dyno-queues 分布式延迟队列 之 辅助功能 目录 Dyno-queues 分布式延迟队列 之 辅助功能 0x00 摘要 0x01 前文回顾 0x2 Ack机制 2.1 加入Un-ack集合 ...
- HTTPS:网络安全攻坚战
本文为<三万长文50+趣图带你领悟web编程的内功心法>第五个章节. 5.HTTPS 我们知道,明文传输和不安全是HTTP的其中一个特点,但是随着越来越多机密的业务交易转移到线上,如银行转 ...
- LDAP + Samba 安装配置流程
LDAP + Samba 安装配置 基础环境:Ubuntu18.04 安装samba root@cky:~# apt install samba smbldap-tools -y 查看版本 root@ ...
- Go benchmark 一清二楚
前言 基准测试(benchmark)是 go testing 库提供的,用来度量程序性能,算法优劣的利器. 在日常生活中,我们使用速度 m/s(单位时间内物体移动的距离)大小来衡量一辆跑车的性能,同理 ...
- Centos7安装Docker&镜像加速
目录 Docker Docker安装 方式一 方式二 docker 镜像加速 Docker Docker安装 Docker安装 方式一 step1: 删除老版本(Uninstall old versi ...