CountDownLatch与CyclicBarrier
对于AbstractQueuedSynchronizer衍生出来的并发工具类,这一篇再介绍俩。
场景1:有4个大文件的数据需要统计,最终将所有的统计结果进行加工,得到最后的分析数据。为了加速处理过程,当然是利用多线程:开启4个线程去分别统计每个文件的数据,开启1个线程对之前4个线程的数据加工。
这里的难点是加工线程必须等到4个统计线程都结束了,才能开始工作,不然加工出来的数据肯定是脏的。
如何保证这一点呢,synchronized、wait-notify、Condition这些机制实现起来好像特别麻烦,需要自己实现很多逻辑的控制。正是基于这个痛点,并发大师提供了一个工具类---CountDownLatch。
先来看看如何使用:
package countdownlatch;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchTest {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(4);
new Thread() {
@Override
public void run() {
System.out.println("加工线程:" + Thread.currentThread().getName() + "开始等待数据");
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据都到齐了!!!");
}
}.start();
for (int i=0; i<4; i++) {
new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始统计数据");
try {
sleep(1000); // 模拟统计过程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "统计完成");
latch.countDown();
}
}.start();
}
}
}
运行结果
加工线程:Thread-0开始等待数据
Thread-1开始统计数据
Thread-2开始统计数据
Thread-3开始统计数据
Thread-4开始统计数据
Thread-1统计完成
Thread-4统计完成
Thread-3统计完成
Thread-2统计完成
数据都到齐了!!!
通过CountDownLatch的await和countDown方法轻松的实现了控制逻辑,从源码进去看看,发现await方法的执行逻辑跟上一篇的Semaphore的逻辑几乎一模一样,只不过tryAcquireShared方法的逻辑实现不一样
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
简单吧。CountDownLatch里面的内部类把从AbstractQueuedSynchronizer继承来的state属性,当作需等待的线程数量,tryAcquireShared方法只是判断这个数量是否到0了。如果没有到0,返回-1,后续会执行AbstractQueuedSynchronizer的doAcquireSharedInterruptibly方法,将当前线程封装成共享模式的节点,添加到等待队列。具体内容不再赘述。
再看看countDown方法的主要逻辑:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0) // c为0说明已经release了,比如说4个已经线程完事,第5个线程又调用了countDown方法,不会产生任何影响
return false;
int nextc = c-1; // 只是减1,参数releases没起作用
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
又见无限循环里面的CAS操作。每个被等待的线程调用一次countDown方法,state减1,最后一次调用的时候,nextc == 0为true,就会执行AbstractQueuedSynchronizer的doReleaseShared方法
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
虽然实现逻辑略显复杂,一句话概括就是,将等待队列里的等待线程都unpark,之前的例子就是将加工线程唤醒。
好了,CountDownLatch就到这里。
场景2:有4个大文件的数据需要处理,每个文件处理过程分2步,1)先检查文件数据是否正确,2)然后统计这个文件的某个指标;但是只要有一个文件检查没有完成(比如数据有错),所有的统计就失去意义。
思路:开启4个线程分别对应每个文件,每个线程执行完步骤1,不能马上执行步骤2,必须等待其他的3个线程都执行完步骤1,然后4个线程才能进行步骤2。
所以这里的难点是如何保证一个线程执行到某一点(步骤1完成),必须等待其他的线程也执行到这个点。
乍一看,好像是一个简单的线程通信问题。但是wait-notify、Condition的唤醒机制是一个线程唤醒另一个(或多个)线程,而这里是互相牵制,一个线程是不知道能不能进行唤醒操作的,因为有别的线程还没有执行完步骤1。
问题变得既抽象又复杂,但是并发大师有完美的解决方案---CyclicBarrier。
看例子
package cyclicbarrier; import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier; public class CyclicBarrierTest { public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(4); for (int i=0; i<4; i++) {
new Thread() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始检查文件");
try {
sleep(2000); // 模拟检查过程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "检查文件结束,等待其他线程");
try {
barrier.await(); // 等待
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "统计");
}
}.start();
}
}
}
运行结果
Thread-2开始检查文件
Thread-3开始检查文件
Thread-0开始检查文件
Thread-1开始检查文件
Thread-0检查文件结束,等待其他线程
Thread-3检查文件结束,等待其他线程
Thread-2检查文件结束,等待其他线程
Thread-1检查文件结束,等待其他线程
Thread-1统计
Thread-2统计
Thread-0统计
Thread-3统计
规整有序结果,说明线程之间的控制很到位,但是我们只是调用了一个await方法,那实现的代码必定复杂,硬着头皮看看
await方法的核心逻辑都在dowait方法里面:
/**
* Main barrier code, covering the various policies.
*/
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();
} int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
} // loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
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();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
这里的实现还是借助了ReentrantLock和Condition,看看CyclicBarrier里面的属性
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation(); /**
* Number of parties still waiting. Counts down from parties to 0
* on each generation. It is reset to parties on each new
* generation or when broken.
*/
private int count;
parties是刚开始互相等待的线程数;count是还在让别的线程等待的线程数;barrierCommand是所有线程都执行到指定位置之后马上执行的任务,就像场景1中最后的加工任务;generation标识分代信息。举个例子,左轮手枪可以装6发子弹,某一时刻,还剩2发;parties就是6,count就是2;当6发子弹全部打出,如果要执行一个动作(比如维修、清洗),这个动作就是barrierCommand;然后需要重新装入子弹,这就是一个换代的过程。
CyclicBarrier引入分代的概念就是想重复利用,Cyclic就是可循环的意思。
再回到dowait方法,参数timed和nanos代表是否考虑超时的问题,大致梳理一下执行流程:
1、先加锁,同一时刻,只有一个线程可以执行后续逻辑。
2、count减1后,如果为0,说明这个线程就是最后一个被等待的线程,就可以执行barrierCommand,然后执行更新换代:先唤醒所有的还在等待的线程,然后将parties、count、generation统统更新;
3、如果不为0,线程自己也会被加入Condition的等待队列
4、时刻要考虑超时问题、中断异常处理、换代的意外等
最后比较一下CountDownLatch和CyclicBarrier:
1、从场景1和场景2来看,这两个工具类的关注点不一样,CountDownLatch关注的是某一类线程等待另一类线程的信号(执行countDown方法),而CyclicBarrier关注的是同一类线程互相等待彼此的信号(执行await方法)
2、CyclicBarrier可以重复使用,而CountDownLatch只能使用一次
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: 定义: 其是一个线程同步的辅助工具,通过它可以做到使一条线程一直阻塞等待,直到其他线程完成其所 ...
随机推荐
- Ztree _ 横向显示子节点、点击文字勾选、去除指定元素input的勾选状态
前些天项目需要树结构表现数据,需求ztree就能满足所以直接使用ztree只是踩了些小坑... 1.ztree子节点横向显示(下图): 效果说明:第三级子节点按需求横向显示其他竖向显示,每行最多显示5 ...
- Backpropagation 算法的推导与直观图解
摘要 本文是对 Andrew Ng 在 Coursera 上的机器学习课程中 Backpropagation Algorithm 一小节的延伸.文章分三个部分:第一部分给出一个简单的神经网络模型和 B ...
- 弹性布局Flex的基本语法
一.Flex的简介 Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性.用六个字概括弹性布局就是简单.方便.快速. flex( fle ...
- 云端TensorFlow读取数据IO的高效方式
低效的IO方式 最近通过观察PAI平台上TensoFlow用户的运行情况,发现大家在数据IO这方面还是有比较大的困惑,主要是因为很多同学没有很好的理解本地执行TensorFlow代码和分布式云端执行T ...
- 【机器学习笔记之四】Adaboost 算法
本文结构: 什么是集成学习? 为什么集成的效果就会好于单个学习器? 如何生成个体学习器? 什么是 Boosting? Adaboost 算法? 什么是集成学习 集成学习就是将多个弱的学习器结合起来组成 ...
- [算法题] Remove Duplicates from Sorted Array ii
题目内容 本题来源LeetCode Follow up for "Remove Duplicates": What if duplicates are allowed at mos ...
- 分享一款好用的PHP下ID混淆插件
前几天接到个任务,甲方先前自己找外包开发的一个产品购买小站不准备继续给外包维护了,转手抛给我们,顺便提了一堆修改意见. 其中一个意见是因为外包开发的订单网站直接用数据库自增的id作为订单号,导致订单号 ...
- python学习===复制list
"""将一个列表的数据复制到另一个列表中.""" """ 使用[:] """ a ...
- HTML5的三种存储方式以及区别
首先将存储方式前要先知道为什么要使用他:一是利用本地数据,介绍网络请求:二是弱网环境下,高延迟,低带宽,要把数据本地化: 1.本地存储localStorage和sessionStorage 介绍: 存 ...
- Python 3.X安装配置
0x01 安装Python 目前,Python有两个版本,一个是2.x版,一个是3.x版,这两个版本是不兼容的. 本教程安装的是python-3.6.1-amd64版本. Python官网:https ...