CountdownLatch,CyclicBarrier是非常常用并发工具类,可以说是Java工程师必会技能了。不但在项目实战中经常涉及,而且在编写压测程序,多线程demo也是必不可少,所以掌握它们的用法和实现原理非常有必要。

念念不忘,必有回响!

点赞走一走,找到女朋友~

等待多线程完成的CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。也就是说通过使用CountDownLatch工具类,可以让一组线程等待彼此执行完毕后在共同执行下一个操作。具体流程如下图所示,箭头表示任务,矩形表示栅栏,当三个任务都到达栅栏时,栅栏后wait的任务才开始执行。

CountDownLatch维护有个int型的状态码,每次调用countDown时状态值就会减1;调用wait方法的线程会阻塞,直到状态码为0时才会继续执行。

在多线程协同工作时,可能需要等待其他线程执行完毕之后,主线程才接着往下执行。首先我们可能会想到使用线程的join方法(调用join方法的线程优先执行,该线程执行完毕后才会执行其他线程),显然这是可以完成的。

使用Thread.join()方法实现

public class RunningRaceTest {
public static void main(String[] args) throws InterruptedException {
Thread runner1 = new Thread(new Runner(), "1号");
Thread runner2 = new Thread(new Runner(), "2号");
Thread runner3 = new Thread(new Runner(), "3号");
Thread runner4 = new Thread(new Runner(), "4号");
Thread runner5 = new Thread(new Runner(), "5号");
runner1.start();
runner2.start();
runner3.start();
runner4.start();
runner5.start(); runner1.join();
runner2.join();
runner3.join();
runner4.join();
runner5.join(); // 裁判等待5名选手准备完毕
System.out.println("裁判:比赛开始~~");
}
} class Runner implements Runnable {
@Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Thread.join()完全可以实现这个需求,不过存在一个问题,如果调用join的线程一直存活,则当前线程则需要一直等待。这显然不够灵活,并且当前线程可能会出现死等的情况。

更加灵活的CountDownLatch

jdk1.5之后的并发包中提供了CountDownLatch并发工具了,也可以实现join的功能,并且功能更加强大。

// 参赛选手线程
class Runner implements Runnable {
private CountDownLatch countdownLatch; public Runner(CountDownLatch countdownLatch) {
this.countdownLatch = countdownLatch;
} @Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 准备完毕,举手示意
countdownLatch.countDown();
}
}
} public class RunningRaceTest {
public static void main(String[] args) throws InterruptedException {
// 使用线程池的正确姿势
int size = 5;
AtomicInteger counter = new AtomicInteger();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(size, size, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), (r) -> new Thread(r, counter.addAndGet(1) + " 号 "), new ThreadPoolExecutor.AbortPolicy()); CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < size; i++) {
threadPoolExecutor.submit(new Runner(countDownLatch));
} // 裁判等待5名选手准备完毕
countDownLatch.await(); // 为了避免死等,也可以添加超时时间
System.out.println("裁判:比赛开始~~"); threadPoolExecutor.shutdownNow();
}
}

输出结果:

5 号  选手已就位, 准备共用时: 20ms
4 号 选手已就位, 准备共用时: 156ms
1 号 选手已就位, 准备共用时: 288ms
2 号 选手已就位, 准备共用时: 519ms
3 号 选手已就位, 准备共用时: 945ms
比赛开始~~

同步屏障CyclicBarrier

CyclicBarrier可以实现CountDownLatch一样的功能,不同的是CountDownLatch属于一次性对象,声明后只能使用一次,而CyclicBarrier可以循环使用

从字面意义上来看,CyclicBarrier表示循环的屏障,当一组线程全部都到达屏障时,屏障才会被移除,否则只能阻塞在屏障处。

public class RunningRace {
public static void main(String[] args) {
// 使用线程池的正确姿势
int size = 5;
AtomicInteger counter = new AtomicInteger();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(size, size, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), (r) -> new Thread(r, counter.addAndGet(1) + " 号 "), new ThreadPoolExecutor.AbortPolicy()); CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("裁判:比赛开始~~"));
for (int i = 0; i < 10; i++) {
threadPoolExecutor.submit(new Runner(cyclicBarrier));
}
}
} class Runner implements Runnable {
private CyclicBarrier cyclicBarrier; public Runner(CyclicBarrier countdownLatch) {
this.cyclicBarrier = countdownLatch;
} @Override
public void run() {
try {
int sleepMills = ThreadLocalRandom.current().nextInt(1000);
Thread.sleep(sleepMills);
System.out.println(Thread.currentThread().getName() + " 选手已就位, 准备共用时: " + sleepMills + "ms" + cyclicBarrier.getNumberWaiting());
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}

由于CyclicBarrier可以循环使用,所以CyclicBarrier的构造方法中可以传入一个Runnable参数,在每一轮执行完毕之后就会立刻执行这个Runnable任务

CountDownLatch设计与实现

CountDownLath是基于AQS框架的一种简单实现,有两个核心的方法,即await()和countDown(),通过构造方法传入一个状态值,调用await()方法时线程会阻塞,直到状态码被修改成0时才会返回,每次调用countDown()时会将状态值减1。

wait方法:执行wait方法后,会尝试获取同步状态,如果为状态为0则方法继续执行,否择当前线程会被加入到同步队列中,详情可见笔者关于AQS的两篇文章。

public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果状态码不为0,尝试获取同步状态,如果失败则被加入到同步队列中
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 当状态码为0时返回1,否择返回-1,这个方法中参数没有任何用处
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}

countDown方法:每次执行countDown方法时,会将状态码的值减1.

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

CyclicBarrier的设计与实现

CyclicBarrier与CountDownLatch实现思想相同,也是基于AQS框架实现。不同的是CyclicBarrier内部维护一个状态值借助基于AQS实现的锁ReentrantLock来实现状态值的同步更新,以及AQS除了同步状态之外的另一个核心概念条件队列来完成线程的阻塞。

parties: 和CountdownLatch中的状态值一样,用来记录每次要相互等待的线程数量,只有parties个线程同时到达屏障时,才会唤醒阻塞的线程。

count临时计数器: 由于CyclicBarrier是可以循环使用的,count可以理解为是一个临时变量,每一轮执行完毕或者被打断都会重置count为parties值。

Generation内部类: 只有一个属性 broken表示当前这一轮执行是否被中断,如果被中断后其他线程再执行await方法会抛出异常(目的是停止本轮线程未执行线程的继续执行)。

await方法: 当执行await方法时,会同步得对内部的count执行--count操作, 如果count = 0,则执行barrierCommand任务(通过构造方法传来的Runnable参数)。

reset方法:中断本轮执行,重置count值,唤醒等待的线程然后开始下一轮,此时本轮正在执行的线程调用await方法会抛出异常。

// await方法实际执行的代码
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 加锁,保证并发操作的一致性
lock.lock();
try {
// 如果当前这一轮操作被中断,抛出中断异常(该异常只是起警示作用,没有任何其他信息)
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 本轮执行的计数器 数值-1
int index = --count;
if (index == 0) { // 计数器值=1, 本轮线程全部到达屏障,执行barrierCommand任务
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();// 唤醒所有等待在条件队列上的任务
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
} // 如果状态不等于0,循环等待直到计数器值为0,本轮执行被打破,线程被中断,或者等待超时
for (;;) {
try {
if (!timed)
// 状态码不为0,将当前线程加入到条件队列中,进入阻塞状态
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
} if (g.broken)
throw new BrokenBarrierException(); if (g != generation)
return index; if (timed && nanos <= 0L) {
breakBarrier();// 唤醒所有条件队列中的线程,重置count的值
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

重置栅栏的状态

public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
/**
* Sets current barrier generation as broken and wakes up everyone.
* Called only while holding lock.
*/
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}

当一轮执行完毕之后,既count=0后,CyclicBarrier的临时状态会重置为parties

/**
* 进入下一轮
* 唤醒所有等待线程,充值count
*/
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}

总结

  1. CountDownLatch创建后只能使用一次,而CyclicBarrier可以循环使用,并且CyclicBarrier功能更完善。
  2. CountDownLatch内部的状态是基于AQS中的状态信息,而CyclicBarrier中的状态值是单独维护的,使用ReentrantLock加锁保证并发修改状态值的数据一致性。
  3. 它们的使用场景:允许一个或多个线程等待其他线程完成操作, 即当指定数量线程执行完某个操作再继续执行下一个操作。

最常用的CountDownLatch, CyclicBarrier你知道多少? (Java工程师必会)的更多相关文章

  1. CountDownLatch CyclicBarrier和 Semaphore

    CountDownLatch CyclicBarrier和 Semaphore 原理 基于AQS实现. 让需要的暂时阻塞的线程,进入一个死循环里面,得到某个条件后再退出循环,以此实现阻塞当前线程的效果 ...

  2. 并发包下常见的同步工具类(CountDownLatch,CyclicBarrier,Semaphore)

    在实际开发中,碰上CPU密集且执行时间非常耗时的任务,通常我们会选择将该任务进行分割,以多线程方式同时执行若干个子任务,等这些子任务都执行完后再将所得的结果进行合并.这正是著名的map-reduce思 ...

  3. java 并发工具类CountDownLatch & CyclicBarrier

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

  4. 并发包下常见的同步工具类详解(CountDownLatch,CyclicBarrier,Semaphore)

    目录 1. 前言 2. 闭锁CountDownLatch 2.1 CountDownLatch功能简介 2.2 使用CountDownLatch 2.3 CountDownLatch原理浅析 3.循环 ...

  5. CountDownLatch/CyclicBarrier/Semaphore 使用过吗?

    CountDownLatch/CyclicBarrier/Semaphore 使用过吗?下面详细介绍用法: 一,(等待多线程完成的)CountDownLatch  背景; countDownLatch ...

  6. Java并发编程工具类 CountDownLatch CyclicBarrier Semaphore使用Demo

    Java并发编程工具类 CountDownLatch CyclicBarrier Semaphore使用Demo CountDownLatch countDownLatch这个类使一个线程等待其他线程 ...

  7. Atitit 图像处理 常用8大滤镜效果 Jhlabs 图像处理类库 java常用图像处理类库

    Atitit 图像处理 常用8大滤镜效果 Jhlabs 图像处理类库 java常用图像处理类库1.1. 5种常用的Photoshop滤镜,分别针对照片的曝光.风格色调.黑白照片处理.锐利度.降噪这五大 ...

  8. atitit.常用编程语言的性能比较 c c++ java

    atitit.常用编程语言的性能比较 c c++ java 选择一个什么样的程序问题进行这样的测试呢?这是一个很关键的问题,也最容易影响测试的公平性.另外的,对于每种语言,各自的优势都是不同的 #-- ...

  9. 常用的排序算法介绍和在JAVA的实现(二)

    一.写随笔的原因:本文接上次的常用的排序算法介绍和在JAVA的实现(一) 二.具体的内容: 3.交换排序 交换排序:通过交换元素之间的位置来实现排序. 交换排序又可细分为:冒泡排序,快速排序 (1)冒 ...

随机推荐

  1. Idea全部快捷键+自行修改快捷键

    Idea常用快捷键 Tab,代码标签输入完成后,按 Tab,生成代码 Ctrl+E,最近的文件 Ctrl+X,删除行 Ctrl+D,复制行 Alt+1,快速打开或隐藏工程面板 ctrl+alt+t 快 ...

  2. TCP/IP详解,卷1:协议--IP:网际协议

    引言 I P 是 T C P / I P 协议族中最为核心的协议.所有的 T C P.U D P.I C M P 及 I G M P 数据都以 I P 数据 报格式传输(见图 1 - 4).许多刚开始 ...

  3. Java:面向对象的编程语言

    java是面向对象的编程语言 Object,就是指面向对象的对象,对象就是实例. 在java里,对象是类的一个具体实例.就像:人,指一个类.你.我.他.张三.李四.王五等则是一个个具体的实例,也就是j ...

  4. c++基础语法规则

    1,c++存储类:定义函数或者变量的生命周期     auto 关键字用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型.声明函数时函数返回值的占位符. register 存储类用于定义存储 ...

  5. JVM 引用类型

    1.强引用 强引用,是在我们的开发工作当中普遍存在的.如果一个对象具有强引用,那就类似我们经常穿的衣服啊等必不可少的生活用品,我们肯定不会把他扔掉,同样jvm的垃圾回收器也不会回收它.当内存空间不足的 ...

  6. Python requests库模拟浏览器行为的一些技巧记录

    如下都是一些经验之谈,不定期更新,喜欢可以关注哦. 忽略ssl报错 一些证书问题会导致程序报错,解决方法为在发送请求的时候,带上verify=False参数即可: result = requests. ...

  7. 有关C/C++中,表达式计算顺序的问题,以及表达式内部变量“副作用”问题(转)

    经常可以在一些讨论组里看到下面的提问:“谁知道下面C语句给n赋什么值?”m = 1; n = m+++m++;最近有位不相识的朋友发email给我,问为什么在某个C++系统里,下面表达式打印出两个4, ...

  8. at org.apache.hadoop.hbase.tmpl.master.BackupMasterStatusTmplImpl.renderNoFlush(BackupMasterStatusTm

    at org.apache.hadoop.hbase.tmpl.master.BackupMasterStatusTmplImpl.renderNoFlush(BackupMasterStatusTm ...

  9. sql server获取查询时间

    declare @d datetime set @d=getdate() /*你的SQL脚本开始*/ /*你的SQL脚本结束*/ select [语句执行花费时间(毫秒)]=datediff(ms,@ ...

  10. 题解【Codeforces1186A】 Vus the Cossack and a Contest

    这题是入门难度的题目吧-- 根据题意可以得出,只有当\(m\)和\(k\)都大于等于\(n\)时,\(Vus\)才可以实现他的计划. 因此,我们不难得出以下\(AC\)代码: #include < ...