Java并发编程笔记之CyclicBarrier源码分析
JUC 中 回环屏障 CyclicBarrier 的使用与分析,它也可以实现像 CountDownLatch 一样让一组线程全部到达一个状态后再全部同时执行,但是 CyclicBarrier 可以被复用。那么 CyclicBarrier 内部的实现与 CountDownLatch 有何不同那?
CounDownLatch在解决多个线程同步方面相对于调用线程的 join 已经提供了不少改进,但是CountDownLatch的计数器是一次性的,也就是等到计数器变为0后,再调用CountDownLatch的await ()和countDown()方法都会立刻返回,这就起不到线程同步的效果了。CyclicBarrier类的功能不限于CountDownLatch所提供的功能,从字面意思理解CyclicBarrier是回环屏障的意思,它可以实现让一组线程全部达到一个状态后再全部同时执行。这里之所以叫做回环是因为当所有等待线程执行完毕之后,重置CyclicBarrier的状态后可以被重用。下图演示了这一过程。
一.CyclicBarrier的实现原理
为了能一览CyclicBarrier的架构设计,下面先看下CyclicBarrier的类图,如下图:
如上面类图,可以知道CyclicBarrier 内部并不是直接使用AQS实现,而是使用了独占锁ReentrantLock来实现的同步;parties用来记录线程个数,用来表示需要多少线程先调用await后,所有线程才会冲破屏障继续往下运行;而 count 一开始等一parties,每当线程调用await方法后就递减 1 ,当为 0 的时候就表示所有线程都到了屏障点,另外你可能会疑惑为何维护parties 和 count 这两个变量,只有count 不就行了吗?别忘了CyclicBarries是可以被复用的,使用两个变量原因是用parties始终来记录总的线程个数,当count计数器变为 0 后,会使用parties 赋值给count,已达到复用的作用。这两个变量是在构造CyclicBarries对象的时候传递的,源码如下:
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= ) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
这里还有一个变量barrierConmmand也通过构造函数传递而来,这是一个任务,这个任务的执行时机是当所有线程都达到屏障点后。另外CyclicBarrier内部使用独占锁Lock来保证同时只有一个线程调用await方法时候才可以返回,使用lock首先保证了更新计数器count 的原子性,另外使用lock的条件变量 trip 支持了 线程间使用 notify,await 操作进行同步。
最后变量generation内部就一个变量broken用来记录当前屏障是否被打破,另外注意这里broken并没有被声明为volatile ,这是因为锁内使用变量不需要。源码如下:
private static class Generation {
boolean broken = false;
}
接下来重点看一下CyclicBarrier的几个重要的函数,如下:
1.int await() 当前线程调用 CyclicBarrier 的该方法时候,当前线程会被阻塞,知道满足下面条件之一才会返回:(1)parties 个线程都调用了 await()方法,也就是线程都到了屏障点。(2)其他线程调用了当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InterruptedException 异常返回。(3)当前屏障点关联的Generation对象的broken标志被设置为true的时候,会抛出 BrokenBarrierException 异常。源码如下:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
正如上面代码可以知道内部调用了dowait 方法,第一个参数false说明不设置超时时间,这时候第二个参数没有意义。
2.boolean await(long timeout, TimeUnit unit) 当前线程调用 CyclicBarrier 的该方法时候当前线程会被阻塞,直到满足下面条件之一才会返回: (1) parties 个线程都调用了 await() 函数,也就是线程都到了屏障点,这时候返回 true。 (2) 当设置的超时时间到了后返回 false (3) 其它线程调用了当前线程的 interrupt()方法中断了当前线程,则当前线程会抛出 InterruptedException 异常返回。 (4) 当前屏障点关联的 Generation 对象的 broken 标志被设置为 true 时候,会抛出 BrokenBarrierException 异常。源码如下:
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
正如上面代码可以知道内部调用了dowait 方法,第一个参数true说明设置超时时间,这时候第二个参数是超时时间。
3.int dowait(boolean timed, long nanos) 该方法是实现 CyclicBarrier 的核心功能,源码如下:
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
... //(1)如果index==0说明所有线程都到到了屏障点,则执行初始化时候传递的任务
int index = --count;
if (index == ) { // tripped
boolean ranAction = false;
try {
//(2)执行任务
if (command != null)
command.run();
ranAction = true;
//(3)激活其它调用await而被阻塞的线程,并重置CyclicBarrier
nextGeneration();
//返回
return ;
} finally {
if (!ranAction)
breakBarrier();
}
} // (4)如果index!=0
for (;;) {
try {
//(5)没有设置超时时间,
if (!timed)
trip.await();
//(6)设置了超时时间
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
...
}
...
}
} finally {
lock.unlock();
}
} private void nextGeneration() {
//(7)唤醒条件队列里面阻塞线程
trip.signalAll();
//(8) 重置CyclicBarrier
count = parties;
generation = new Generation();
}
上面代码是dowait方法的主干代码,当一个线程调用了dowait方法后首先会获取独占锁lock,如果创建CyclicBarrier的时候传递的参数为 10 ,那么后面 9 个调用线程会被阻塞;然后当前获取线程对计数器count进行递减操作,递减后的count = index = 9 ,因为 index != 0 ,所以当前线程会执行代码(4)。如果是无参数的当前线程调用的是无参数的await()方法,则这里 timed = false,所以当前线程会被放入条件变量trip的阻塞队列,当前线程会被挂起并释放获取的Lock锁;如果调用的有参数的await 方法 则timed = true,则当前线程也会被放入条件变量阻塞队列并释放锁的资源,但是不同的是当前线程会在指定时间超时后自动激活。
当第一个获取锁的线程由于被阻塞释放锁后,被阻塞的 9 个线程中有一个会竞争到lock锁,然后执行第一个线程同样的操作,直到最后一个线程获取到lock的时候,已经有 9 个线程被放入了Lock 的条件队列里面,最后一个线程 count 递减后,count = index 等于 0 ,所以执行代码(2),如果创建CyclicBarrier的时候传递了任务,则在其他线程被唤醒前先执行任务,任务执行完毕后再执行代码(3),唤醒其他 9 个线程,并重置CyclicBarrier,然后这 10个线程就可以继续向下执行了。
到目前位置理解了CyclicBarrier的原理后,接下来用几个例子来加深对CyclicBarrier的理解,下面例子我们要实现的是使用两个线程去执行一个被分解的任务 A,当两个线程把自己的任务都执行完毕后在对它们的结果进行汇总处理。例子如下:
package com.hjc; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* Created by cong on 2018/7/7.
*/
public class CyclicBarrierTest1 { // 创建一个CyclicBarrier实例,添加一个所有子线程全部到达屏障后执行的一个任务
private static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(, new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + " task1 merge result");
}
}); public static void main(String[] args) throws InterruptedException { //创建一个线程个数固定为2的线程池
ExecutorService executorService = Executors.newFixedThreadPool(); // 加入线程A到线程池
executorService.submit(new Runnable() {
public void run() {
try { System.out.println(Thread.currentThread() + " task1-1"); System.out.println(Thread.currentThread() + " enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + " enter out barrier"); } catch (Exception e) {
e.printStackTrace();
}
}
}); // 加入线程B到线程池
executorService.submit(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread() + " task1-2"); System.out.println(Thread.currentThread() + " enter in barrier");
cyclicBarrier.await();
System.out.println(Thread.currentThread() + " enter out barrier"); } catch (Exception e) {
e.printStackTrace();
}
}
}); // 关闭线程池
executorService.shutdown();
}
}
运行结果如下:
如上代码创建了一个 CyclicBarrier 对象,第一个参数为计数器初始值,第二个参数 Runable 是指当计数器为 0 时候需要执行的任务。main 函数里面首先创建了固定大小为 2 的线程池,然后添加两个子任务到线程池,每个子任务在执行完自己的逻辑后会调用 await 方法。
一开始计数器为 2,当第一个线程调用 await 方法时候,计数器会递减为 1,由于计数器不为 0,所以当前线程就到了屏障点会被阻塞,然后第二个线程调用 await 时候,会进入屏障,计数器也会递减现在计数器为 0,就会去执行在 CyclicBarrier 构造时候的任务,执行完毕后就会退出屏障点,并且会唤醒被阻塞的第一个线程,这时候第一个线程也会退出屏障点继续向下运行。
上面的例子说明了多个线程之间是相互等待的,假如计数器为 N,那么调用 await 方法的前面 N-1 的线程都会因为到达屏障点被阻塞,当第 N 个线程调用 await 后,计数器为 0 了,这时候第 N 个线程才会发出通知唤醒前面的 N-1 个线程。也就是全部线程达到屏障点时候才能一块继续向下执行,对与这个例子来说使用 CountDownLatch 也可以达到类似输出结果。
下面在放个例子来说明 CyclicBarrier 的可复用性。
假设一个任务由阶段 1、阶段 2、阶段 3 组成,每个线程要串行的执行阶段 1 和 2 和 3,多个线程执行该任务时候,必须要保证所有线程的阶段 1 全部完成后才能进行阶段 2 执行,所有线程的阶段 2 全部完成后才能进行阶段 3 执行,下面使用 CyclicBarrier 来完成这个需求。例子如下:
package com.hjc; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* Created by cong on 2018/7/7.
*/
public class CyclicBarrierTest2 { // 创建一个CyclicBarrier实例
private static volatile CyclicBarrier cyclicBarrier = new CyclicBarrier(); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(); // 加入线程A到线程池
executorService.submit(new Runnable() {
public void run() {
try { System.out.println(Thread.currentThread() + " step1");
cyclicBarrier.await(); System.out.println(Thread.currentThread() + " step2");
cyclicBarrier.await(); System.out.println(Thread.currentThread() + " step3"); } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}); // 加入线程B到线程池
executorService.submit(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread() + " step1");
cyclicBarrier.await(); System.out.println(Thread.currentThread() + " step2");
cyclicBarrier.await(); System.out.println(Thread.currentThread() + " step3"); } catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}); //关闭线程池
executorService.shutdown();
}
}
运行结果如下:
如上代码,在每个子线程执行完 step1 后都调用了 await 方法,所有线程都到达屏障点后才会一块往下执行,这就保证了所有线程完成了 step1 后才会开始执行 step2,然后在 step2 后面调用了 await 方法,这保证了所有线程的 step2 完成后,线程才能开始 step3 的执行,这个功能使用单个 CountDownLatch 是无法完成的。
Java并发编程笔记之CyclicBarrier源码分析的更多相关文章
- Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝 ...
- Java并发编程笔记之ThreadLocalRandom源码分析
JDK 并发包中 ThreadLocalRandom 类原理剖析,经常使用的随机数生成器 Random 类的原理是什么?及其局限性是什么?ThreadLocalRandom 是如何利用 ThreadL ...
- Java并发编程笔记之ThreadLocal源码分析
多线程的线程安全问题是微妙而且出乎意料的,因为在没有进行适当同步的情况下多线程中各个操作的顺序是不可预期的,多线程访问同一个共享变量特别容易出现并发问题,特别是多个线程需要对一个共享变量进行写入时候, ...
- Java并发编程笔记之FutureTask源码分析
FutureTask可用于异步获取执行结果或取消执行任务的场景.通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过Fu ...
- Java并发编程笔记之SimpleDateFormat源码分析
SimpleDateFormat 是 Java 提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个 SimpleDateFormat 实例对日期进行 ...
- Java并发编程笔记之Timer源码分析
timer在JDK里面,是很早的一个API了.具有延时的,并具有周期性的任务,在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做,但是Timer存在一 ...
- Java并发编程笔记之PriorityBlockingQueue源码分析
JDK 中无界优先级队列PriorityBlockingQueue 内部使用堆算法保证每次出队都是优先级最高的元素,元素入队时候是如何建堆的,元素出队后如何调整堆的平衡的? PriorityBlock ...
- Java并发编程笔记之ArrayBlockingQueue源码分析
JDK 中基于数组的阻塞队列 ArrayBlockingQueue 原理剖析,ArrayBlockingQueue 内部如何基于一把独占锁以及对应的两个条件变量实现出入队操作的线程安全? 首先我们先大 ...
- Java并发编程笔记之ReentrantLock源码分析
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞后放入该锁的AQS阻塞队列里面. 首先我们先看一下ReentrantLock的类图结构,如下图所示 ...
随机推荐
- py2和py3的区别总结
1.编码 python2默认编码方式ASCII码(不能识别中文,要在文件头部加上 #-*- encoding:utf-8 -*- 指定编码方式) python3默认编码方式unicode(可识别中 ...
- FT_ND_API.dll
ePass1000ND https://blog.csdn.net/li34442779/article/details/44276989 https://www.cnblogs.com/lidabo ...
- jquery 判断 元素是否具有某个class
两种方法如下: 1.hasClass(‘classname’) 2.is(‘.classname’) 例子: 1.使用is(‘.classname’)的方法 $('div').is('.redColo ...
- poj2240
一个关于套利的题,就是判断是否有正环,我这里是用的SPFA,只要判断出来一种货币初始为1,最后变得大于1就代表是正环,要注意一下最后对vector的清空,当时从1开始清空,导致wa了两次,找了半天,尽 ...
- S 实现精确加减乘除
//加法函数 function accAdd(arg1, arg2) { var r1, r2, m; try { r1 = arg1.toString().split(".")[ ...
- explain 类型分析
all 全表扫描 ☆ index 索引全扫描 ☆☆ range 索引范围扫描,常用语<,<=,>=,between等操作 ☆☆☆ ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单 ...
- 单台机器安装zookeeper
先给一堆学习文档,方便以后查看 官网文档地址大全: OverView(概述) http://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html Get ...
- 1.2万事开头hello world+交互+getpass、sys模块初识
1.python的hello world: ①运行cmd-输入python-输入print (“hello world!”) ②创造.py的文本helloworld.py(后缀是为了告诉其他人)-输入 ...
- Android -Services 使用简介
Android Services 四大组件之一,主要用于后台长时间运行.没有界面.这里讲解两种services的启动还有AIDL通信方式. 1.startservices a.建立继承services ...
- ReSharper 10.0.0.1 Ultimate 完美破解补丁使用方法
转自:http://www.leavescn.com/Page/Content.aspx?id=94 ReSharper 10.0.0.1 Ultimate 完美破解补丁使用方法,本资源来自互联网,感 ...