对于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的更多相关文章

  1. Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

    Java并发编程:CountDownLatch.CyclicBarrier和Semaphore 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch ...

  2. 并发工具类:CountDownLatch、CyclicBarrier、Semaphore

    在多线程的场景下,有些并发流程需要人为来控制,在JDK的并发包里提供了几个并发工具类:CountDownLatch.CyclicBarrier.Semaphore. 一.CountDownLatch ...

  3. Java并发(8):CountDownLatch、CyclicBarrier、Semaphore、Callable、Future

    CountDownLatch.CyclicBarrier.Semaphore.Callable.Future  都位于java.util.concurrent包下,其中CountDownLatch.C ...

  4. 【Java多线程】JUC包下的工具类CountDownLatch、CyclicBarrier和Semaphore

    前言 JUC中为了满足在并发编程中不同的需求,提供了几个工具类供我们使用,分别是CountDownLatch.CyclicBarrier和Semaphore,其原理都是使用了AQS来实现,下面分别进行 ...

  5. CountDownLatch,CyclicBarrier,Semaphore

    CountDownLatch是倒数,doneSignal = new CountDownLatch(LATCH_SIZE);赋初值后,在主线程中等待doneSignal.await();其它线程中,每 ...

  6. CountDownLatch、CyclicBarrier、Semaphore、Exchanger

    CountDownLatch: 允许N个线程等待其他线程完成执行.无法进行重复使用,只能用一次. 比如有2个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch ...

  7. CountDownLatch、CyclicBarrier和Semaphore

    转载:http://www.cnblogs.com/dolphin0520/p/3920397.html 在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDown ...

  8. CountDownLatch和CyclicBarrier的区别

    [CountDownLatch.CyclicBarrier和Semaphore]http://www.cnblogs.com/dolphin0520/p/3920397.html   [CountDo ...

  9. 使用Java辅助类(CountDownLatch、CyclicBarrier、Semaphore)并发编程

    在java 1.5中,提供了一些非常有用的辅助类来帮助我们进行并发编程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我们就来学习一下这三个辅助类的用法 一.C ...

  10. CountDownLatch 和 CyclicBarrier 的运用及实现原理

    I.CountDownLatch 和 CyclicBarrier 的运用 CountDownlatch: 定义: 其是一个线程同步的辅助工具,通过它可以做到使一条线程一直阻塞等待,直到其他线程完成其所 ...

随机推荐

  1. 一次浴火重生的MySQL优化(EXPLAIN命令详解)

    一直对SQL优化的技能心存无限的向往,之前面试的时候有很多面试官都会来一句,你会优化吗?我说我不太会,这时可能很多人就会有点儿说法了,比如会说不要使用通配符*去检索表.给常常使用的列建立索引.还有创建 ...

  2. javascript编程解决黑白卡片问题

    问题描述: 时间限制:1秒 空间限制:32768K 牛牛有n张卡片排成一个序列.每张卡片一面是黑色的,另一面是白色的.初始状态的时候有些卡片是黑色朝上,有些卡片是白色朝上.牛牛现在想要把一些卡片翻过来 ...

  3. ASP.NET MVC 开发微信支付H5(外置浏览器支付)

    H5支付是指商户在微信客户端外的移动端网页展示商品或服务,用户在前述页面确认使用微信支付时,商户发起本服务呼起微信客户端进行支付. 主要用于触屏版的手机浏览器请求微信支付的场景.可以方便的从外部浏览器 ...

  4. Python序列化和反序列化

    Python序列化和反序列化 通过将对象序列化可以将其存储在变量或者文件中,可以保存当时对象的状态,实现其生命周期的延长.并且需要时可以再次将这个对象读取出来.Python中有几个常用模块可实现这一功 ...

  5. 最大流当前弧优化Dinic模板

    最大流模板: 普通最大流 无向图限制:将无向图的边拆成2条方向相反的边 无源汇点有最小流限制的最大流:理解为水管流量形成循环,每根水管有流量限制,并且流入量等于流出量 有源汇点的最小流限制的最大流 顶 ...

  6. 常用颜色RGB、灰度值

    128/0/0       深红         255/0/0       红           255/0/255     粉红        255/153/204 玫瑰红       153 ...

  7. python进阶(4):初始面向对象

    一切皆对象! 面向过程-->面向对象 面向过程:根据业务逻辑从上到下堆叠代码 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 面向对象:对函数进行分类和封装,让开发“更快更 ...

  8. JS进阶 ] 分析JS中的异步操作

    写在前面 JS因为是单线程的,所以在执行事务的时候,往往会因为某个事务的延迟,而导致服务器假死,这时候异步编程就显的格外重要,但是异步编程一般理解为回调函数callback,典型的就是node,回调函 ...

  9. python 基础-----数字,字符串,列表,字典类型简单介绍

    一.第一个python小程序 1.下载安装python2.7和python3.6的版本及pycharm,我们可以再解释器中输入这样一行代码: 则相应的就打出了一句话.这里的print是打印的意思.你输 ...

  10. 隐马尔可夫模型(HMM)

    隐马尔可夫模型 (Hidden Markov Model,HMM) 最初由 L. E. Baum 和其它一些学者发表在一系列的统计学论文中,随后在语言识别,自然语言处理以及生物信息等领域体现了很大的价 ...