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: 定义: 其是一个线程同步的辅助工具,通过它可以做到使一条线程一直阻塞等待,直到其他线程完成其所 ... 
随机推荐
- OWASP 之 HTML Injection
			Summary HTML injection is a type of injection issue that occurs when a user is able to control an in ... 
- 使用递归算法结合数据库解析成java树形结构
			使用递归算法结合数据库解析成java树形结构 1.准备表结构及对应的表数据a.表结构: create table TB_TREE ( CID NUMBER not null, CNAME VARCHA ... 
- nodejs-基础
			01-nodejs介绍 1.什么是nodejs 1.(javascript跑在机器端,服务端)Javascript on the machine 2.(跑在谷歌v8引擎上)A runtime for ... 
- Python爬虫从入门到放弃(二十)之 Scrapy分布式原理
			关于Scrapy工作流程回顾 Scrapy单机架构 上图的架构其实就是一种单机架构,只在本机维护一个爬取队列,Scheduler进行调度,而要实现多态服务器共同爬取数据关键就是共享爬取队列. 分布式架 ... 
- 2017-6-4  CTF解题报告
			1.签到题 附件 扫描二维码得到 ZCTF{WELCOME_TO_20-209} 2.阿斯克的秘密 从前有个叫做阿斯克的人,他写了一句话,聪明的你能明白他写的是什么吗? 附件 int a; while ... 
- 【我的漫漫跨考路】有生之年·调完了BUG--冒泡排序C++版本
			正文之前 今天去牛客网试了试一些实战编程题,感觉贼有意思,但是也很难,挑了个成绩排序的算法题我就开始怼! 对我一个编程经验并不是很丰富的人来说,确实算是个挑战了. 所以我满满当当的搞了四个小时多,才算 ... 
- 媒体查询Media Queries详解
			@media 标签可以说是响应式网页的开发基础.其主要由媒体类型(Media Type)和 媒体特性(Media Query)两部分组成. Media Type 设定后面规则生效的展示类型,包括all ... 
- C++之STL总结精华笔记
			一.一般介绍 STL(StandardTemplate Library),即标准模板库,是一个具有工业强度的,高效的C++程序库.它被容纳于C++标准程 ... 
- iOS9中关于 NSURLSession/NSURLConnection HTTP load failed 的解决办法
			最近为了新的存管app上线,忙了近一个月,重新过了一段996的日子,今天终于可以喘口气,继续更新博客了.本文记录一下在iOS 9中发送https请求遇到的问题及解决办法,希望通过本文,可以对ATS的配 ... 
- HTML5中的DOM新特性
			元素下的classList属性 classList属性下面有四个方法: add(value): 添加,已存在的属性不添加 remove(value):删除属性 contain(value):检测属性是 ... 
