前言

下面介绍协调让多线程步调一致的两个工具类:CountDownLatchCyclicBarrier

CountDownLatch和CyclicBarrier的用途介绍

CountDownLatch

// API
void await(); // 使当前线程在闭锁计数器到零之前一直等待,除非线程被中断。
boolean await(long timeout, TimeUnit unit); // 使当前线程在闭锁计数器至零之前一直等待,除非线程被中断或超出了指定的等待时间。
void countDown(); // 递减闭锁计数器,如果计数到达零,则释放所有等待的线程。
long getCount(); // 返回当前计数。
String toString(); // 返回标识此闭锁及其状态的字符串。

CountDownLatch是一个同步工具类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。可以指定计数初始化CountDownLatch,当调用countDown()方法后,在当前计数到达零之前,await()方法会一直受阻塞。计数到达零之后,所有被阻塞的线程都会被释放,await()的所有后续调用都会立即返回。CountDownLatch的计数只能被使用一次,如果需要重复计数使用,则要考虑使用CyclicBarrier

CountDownLatch的用途有很多。将计数为1初始化的CountDownLatch可用作一个简单的开/关或入口:在通过调用countDown()的线程打开入口前,所有调用await()的线程都一直在入口出等待。而用N初始化CountDownLatch可以使一个线程在N个线程完成某项操作之前一直等待,或者使其在某项操作完成N次之前一直等待。

COuntDownLatch的内存一致性语义:线程中调用 countDown() 之前的操作 Happens-Before紧跟在从另一个线程中对应 await() 成功返回的操作。

CyclicBarrier

// API
int await(); // 线程将一直等待直到所有参与者都在此 barrier 上调用 await 方法
int await(long timeout, TimeUnit unit); // 线程将一直等待直到所有参与者都在此 barrier 上调用 await 方法, 或者超出了指定的等待时间。
int getNumberWaiting(); // 返回当前在屏障处等待的参与者数目。
int getParties(); // 返回要求启动此 barrier 的参与者数目。
boolean isBroken(); // 查询此屏障是否处于损坏状态。
void reset(); // 将屏障重置为其初始状态。

CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点(barrier也可被翻译为栅栏) (common barrier point)。 CyclicBarrier 适用于在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待的情况。即所有线程都必须到达屏障位置后,下面的程序才能继续执行,适于在迭代算法中使用。因为 barrier 在释放等待线程后可以计数器会被重置可继续使用,所以称它为循环 的 barrier。

CyclicBarrier支持一个可选的 Runnable命令(也就是可以传入一个线程执行其他操作),在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令将只在每个 barrier point 运行一次。这对所有参与线程继续运行之前更新它们的共享状态将十分有用。

CyclicBarrier的内存一致性语义:线程中调用 await() 之前的操作 Happens-Before 那些是屏障操作的一部份的操作,后者依次 Happens-Before 紧跟在从另一个线程中对应 await() 成功返回的操作。

Actions in a thread prior to calling await() happen-before actions that are part of the barrier action, which in turn happen-before actions following a successful return from the corresponding await() in other threads.

在对账系统中使用CountDownLatch和CyclicBarrier

对账系统流程图如下:

目前对账系统的处理流程是:先查询订单,然后查询派送单,之后对比订单和派送单,将差异写入差异库。对账系统的代码抽象后如下:

while(存在未对账订单){
// 查询未对账订单
pos = getPOrders();
// 查询派送单
dos = getDOrders();
// 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}

利用并行优化对账系统

目前的对账系统,由于订单量和派送单量巨大,所以查询未对账订单getPOrder()和查询派送单getDOrder()都相对比较慢。目前对账系统是单线程执行的,示意图如下(图来自参考[1]):

对于串行化的系统,优化性能首先想到的就是能否利用多线程并行处理

如果我们能将getPOrders()和getDOrders()这两个操作并行处理,那么将会提升效率很多。因为这两个操作并没有先后顺序的依赖,所以,我们可以并行处理这两个耗时的操作。

并行后的示意图如下(图来自参考[1]):

对比单线程的执行示意图,我们发现在同等时间里,并行执行的吞吐量近乎单线程的2倍,优化效果还是相对明显的。

优化后的代码如下:

while(存在未对账订单){
// 查询未对账订单
Thread T1 = new Thread(()->{
pos = getPOrders();
});
T1.start(); // 查询派送单
Thread T2 = new Thread(()->{
dos = getDOrders();
});
T2.start(); // 要等待线程T1和T2执行完才能执行check()和save()这两个操作
// 通过调用T1.join()和T2.join()来实现等待
// 当T2和T2线程退出时,调用T1.jion()和T2.join()的主线程就会从阻塞态被唤醒,从而执行check()和save()
T1.join();
T2.join(); // 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}

使用CountDownLatch实现线程等待

上面的解决方案美中不足的地方在于:每一次while循环都会创建新的线程,而线程的创建是一个耗时操作。所以,最好能使创建出来的线程能够循环使用。一个自然而然的方案便是线程池。

// 创建 2 个线程的线程池
Executor executor =Executors.newFixedThreadPool(2);
while(存在未对账订单){
// 查询未对账订单
executor.execute(()-> {
pos = getPOrders();
}); // 查询派送单
executor.execute(()-> {
dos = getDOrders();
}); /* ??如何实现等待??*/ // 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}   

于是我们就创建两个固定大小为2的线程池,之后在while循环里重复利用。

但是问题也出来了:主线程如何得知getPOrders()和getDOrders()这两个操作什么时候执完?

前面主线程通过调用线程T1和T2的join()方法来等待T1和T2退出,但是在线程池的方案里,线程根本就不会退出,所以,join()方法不可取。

这时我们就可以使用CountDownLatch工具类,将其初始计数值设置为2。当执行完pos = getPOrders();后,将计数器减一,执行完dos = getDOrders();后也将计数器减一。当计数器为0时,被阻塞的主线程就可以继续执行了。

// 创建 2 个线程的线程池
Executor executor = Executors.newFixedThreadPool(2); while(存在未对账订单){
// 计数器初始化为 2
CountDownLatch latch = new CountDownLatch(2);
// 查询未对账订单
executor.execute(()-> {
pos = getPOrders();
latch.countDown();    // 实现对计数器减1
}); // 查询派送单
executor.execute(()-> {
dos = getDOrders();
latch.countDown();    // 实现对计数器减1
}); // 等待两个查询操作结束
latch.await(); // 在await()返回之前,主线程会一直被阻塞 // 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}

使用CyclicBarrier进一步优化对账系统

除了getPOrders()和getDOrders()这两个操作可以并行,这两个查询操作和check()save()这两个对账操作之间也可以并行。

两次查询操作和对账操作并行,对账操作还依赖查询操作的结果,有点像生产者-消费者的意思,两次查询操作是生产者,对账操作是消费者。那么,我们就需要一个队列,来保存生产者生产的数据,而消费者则从这个队列消费数据。

不过,针对对账系统,可以设计两个队列,并且这两个队列之间还有对应关系。订单查询操作将订单查询结果插入订单队列,派送单查询操作将派送单插入派送单队列,这两个队列的元素之间是有一一对应关系。这样的好处在于:对账操作可以每次从订单队列出一个元素和从派送单队列出一个元素,然后对这两个元素执行对账操作,这样数据一定不会乱掉。

如何使两个队列实现完全的并行?

两个查询操作所需时间并不相同,那么一个简单的想法便是,一个线程T1执行订单的查询工程,一个线程T2执行派送单的查询工作,仅当线程T1和T2各自都生产完1条数据的时候,通知线程T3执行对账操作。

先查询完的一方需要在设置的屏障点等待另一方,直到双方都到达屏障点,才开始继续下一步任务。

于是我们可以使用CyclicBarrier来实现这个功能。创建一个计数器初始值为2的CyclicBarrier,同时传入一个回调函数,当计数器减为0的时候,便调用这个函数。

Vector<P> pos; // 订单队列
Vector<D> dos; // 派送单队列
// 执行回调的线程池
// 固定线程数量为1是因为只有单线程取获取两个队列中的数据才不会出现数据匹配不一致问题
Executor executor = Executors.newFixedThreadPool(1);
// 创建CyclicBarrier的计数器为2,传入一个线程另外执行对账操作
// 当计数器为0时,会运行传入线程执行对账操作
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{
executor.execute(()->check());
});
void check(){
P p = pos.remove(0); // 从订单队列中获取订单
D d = dos.remove(0); // 从派送单队列中获取派送单
// 执行对账操作
diff = check(p, d);
// 差异写入差异库
save(diff);
} void checkAll(){
// 循环查询订单库
Thread T1 = new Thread(()->{
while(存在未对账订单){
pos.add(getPOrders()); // 查询订单库
barrier.await(); // 将计数器减一并等待直到计数器为0
}
});
T1.start();
// 循环查询运单库
Thread T2 = new Thread(()->{
while(存在未对账订单){
dos.add(getDOrders()); // 查询运单库
barrier.await(); // 将计数器减一并等待直到计数器为0
}
});
T2.start();
}

线程T1负责查询订单,当查出一条时,调用barrier.await()来将计数器减1,同时等待计数器变为0;线程T2负责查询派送订单,当查出一条时,也调用barrier.await()来将计数器减1,同时等待计数器变为0;当T1和T2都调用barrier.await()时,计数器就会减到0,此时T1和T2就可以执行下一条语句了,同时会调用barrier的回调函数来执行对账操作。

CyclicBarrier的计数器有自动重置的功能,当减到0时,会自动重置你设置的初始值。于是,我们便可以重复使用CyclicBarrier。

小结

CountDownLatchCyclicBarrier是Java并发包提供的两个非常易用的线程同步工具类。它们的区别在于:CountDownLatch主要用来解决一个线程等待多个线程的场景(计数器一旦减到0,再有线程调用await(),该线程会直接通过,计数器不会被重置);CyclicBarrier是一组线程之间的相互等待(计数器可以重用,减到0会重置为设置的初始值),还可以传入回调函数,当计数器为0时,执行回调函数。

参考:

[1] 极客时间专栏王宝令《Java并发编程实战》

[2] Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016

[3] Oracle Java API.https://docs.oracle.com/javase/8/docs/api/index.html?overview-summary.html

【Java并发工具类】CountDownLatch和CyclicBarrier的更多相关文章

  1. 25.大白话说java并发工具类-CountDownLatch,CyclicBarrier,Semaphore,Exchanger

    1. 倒计时器CountDownLatch 在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join ...

  2. JAVA并发工具类---------------(CountDownLatch和CyclicBarrier)

    CountDownLatch是什么 CountDownLatch,英文翻译为倒计时锁存器,是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 闭锁可以延迟线程的进 ...

  3. Java 并发工具类 CountDownLatch、CyclicBarrier、Semaphore、Exchanger

    本文部分摘自<Java 并发编程的艺术> CountDownLatch CountDownLatch 允许一个或多个线程等待其他线程完成操作.假设现有一个需求:我们需要解析一个 Excel ...

  4. Java并发工具类 - CountDownLatch

    Java并发工具类 - CountDownLatch 1.简介 CountDownLatch是Java1.5之后引入的Java并发工具类,放在java.util.concurrent包下面 http: ...

  5. Java并发工具类CountDownLatch源码中的例子

    Java并发工具类CountDownLatch源码中的例子 实例一 原文描述 /** * <p><b>Sample usage:</b> Here is a pai ...

  6. java 并发工具类CountDownLatch & CyclicBarrier

    一起在java1.5被引入的并发工具类还有CountDownLatch.CyclicBarrier.Semaphore.ConcurrentHashMap和BlockingQueue,它们都存在于ja ...

  7. Java中的并发工具类(CountDownLatch、CyclicBarrier、Semaphore、Exchanger)

    在JDK的并发包里提供了很多有意思的并发工具类.CountDownLatch.CyclicBarrier和Semaphore 工具类提供了一种并发流程控制的手段,Exchanger 工具类则提供了在线 ...

  8. java多线程10:并发工具类CountDownLatch、CyclicBarrier和Semaphore

    在JDK的并发包(java.util.concurrent下)中给开发者提供了几个非常有用的并发工具类,让用户不需要再去关心如何在并发场景下写出同时兼顾线程安全性与高效率的代码. 本文分别介绍Coun ...

  9. 多线程学习笔记六之并发工具类CountDownLatch和CyclicBarrier

    目录 简介 CountDownLatch 示例 实现分析 CountDownLatch与Thread.join() CyclicBarrier 实现分析 CountDownLatch和CyclicBa ...

  10. 1.3.4 并发工具类CountDownLatch/Semaphore/CyclicBarrier/FutureTask

    CountDownLatch的2个用途: 1. 所有线程都到达相同的起跑线后,再一起开始跑(并非同时开始,而是队列中一个唤醒另一个)[此情况需到达起跑线后再调用await()等待其他线程] 2. 所有 ...

随机推荐

  1. html 贪吃蛇代码

    最近在搞自己的网站,维护的时候准备放个贪吃蛇上去,顶一下原有的页面. 这个贪吃蛇有一点毒.原来设定了100级:100级刚开局就挂了.后来改掉了选项菜单,修复了. 还有什么bug,欢迎点击侧边的QQ按钮 ...

  2. EFCore Database-first深入研究

    EFCore Database-first深入研究 使用Scaffold-DbContext从数据库生成实体 说明文档: 关于 Scaffold-DbContext 微软有官方说明文档 https:/ ...

  3. SharePoint REST 上传文件请求403错误

    最近,需要在SharePoint上传文件到文档库,但是,上传的过程报错了. 错误代码 { "error": { "code": "-213057525 ...

  4. python中property和setter装饰器

    property和setter装饰器 作用:调用方法改为调用对象, 比如 : p.set_name()     改为   p.set_name 区别:  前者改变get方法,后者改变set方法 效果图 ...

  5. RabbitMQ入门(二)工作队列

      在文章RabbitMQ入门(一)之Hello World,我们编写程序通过指定的队列来发送和接受消息.在本文中,我们将会创建工作队列(Work Queue),通过多个workers来分配耗时任务. ...

  6. 异常java.lang.NoSuchMethodError: org.springframework.core.GenericTypeResolver.resolveTypeArguments(Ljava/lang/Class;Ljava/lang/Class;)[Ljava/lang/Class;

    java.lang.NoSuchMethodError: org.springframework.core.GenericTypeResolver.resolveTypeArguments(Ljava ...

  7. python+win32--com实现excel自动化

    import win32com  APP_TYPE = 'Excel.Application'  xlBlack,xlRed,xlGray,xlBlue = 1,3,15,41 xlBreakFull ...

  8. Linux 系统常用管理命令(精简笔记)

    Linux是一套免费使用和自由传播的类Unix操作系统,下面的笔记是我从鸟菜中摘抄出来的重要命令,并进行了一定的排版,摒弃了一些用不到的乱七八糟的命令,目的是在生产环境中能够快速的定位并查询需要命令的 ...

  9. 简单看看LockSupport和AQS

    这次我们可以看看并发中锁的原理,大概会说到AQS,ReentrantLock,ReentrantReadWriteLock以及JDK8中新增的StampedLock,这些都是在java并发中很重要的东 ...

  10. 解决warning: #181-D: argument is incompatible with corresponding format string conversion警告

    uint8_t NetRSSI=0;uint8_t NetBer=0;uint8_t failtime=0; sscanf(&USART_RX_BUF[0],"%*s%u,%u&qu ...