CyclicBarrier概述

CyclicBarrier可以理解为Cyclic + Barrier, 可循环使用 + 屏障嘛。

  • 之所以是Cyclic的,是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。
  • 之所以叫Barrier,是因为线程调用await方法后就会被阻塞,阻塞点就叫做屏障点。

可以让一组线程全部到达一个屏障【同步点】,再全部冲破屏障,继续向下执行。

案例学习

public class CycleBarrierTest2 {

    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(
2, // 计数器的初始值
new Runnable() { // 计数器值为0时需要执行的任务
@Override
public void run () {
System.out.println(Thread.currentThread() + " tripped ~");
}
}
); public static void main (String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@SneakyThrows
@Override
public void run () {
Thread thread = Thread.currentThread();
System.out.println(thread + " step 1");
cyclicBarrier.await();
System.out.println(thread + " step 2");
cyclicBarrier.await();
System.out.println(thread + " step 3");
}
}); executorService.submit(new Runnable() {
@SneakyThrows
@Override
public void run () {
Thread thread = Thread.currentThread();
System.out.println(thread + " step 1");
cyclicBarrier.await();
System.out.println(thread + " step 2");
cyclicBarrier.await();
System.out.println(thread + " step 3");
}
}); executorService.shutdown();
} }

测试结果如下:

Thread[pool-1-thread-2,5,main] step 1
Thread[pool-1-thread-1,5,main] step 1
Thread[pool-1-thread-1,5,main] tripped ~
Thread[pool-1-thread-1,5,main] step 2
Thread[pool-1-thread-2,5,main] step 2
Thread[pool-1-thread-2,5,main] tripped ~
Thread[pool-1-thread-2,5,main] step 3
Thread[pool-1-thread-1,5,main] step 3
  • 创建了一个CyclicBarrier,指定parties为2作为初始计数值,指定Runnable任务作为所有线程到达屏障点时需要执行的任务。
  • 创建了一个大小为2的线程池,向线程池中提交两个任务,我们根据测试结果来说明这一过程。
  • thread2线程率先执行await(),此时计数值减1,并不为0,因此thread2线程到达屏障点,陷入阻塞。
  • thread1线程之后执行await(),此时计数值减1后为0,接着执行构造器中指定的任务,打印tripped,执行完后退出屏障点,唤醒thread2。
  • 可以看到并不是和CountdownLatch一样是一次性的,而是可重复使用的,退出屏障点后,计数值又被设置为2,之后又重复之前的步骤。

多个线程之间是相互等待的,加入当前计数器值为N,之后N-1个线程调用await方法都会达到屏障点而阻塞,只有当第N个线程调用await方法时,计数器值为0,第N个线程才会唤醒之前等待的所有线程,再一起向下执行。

CyclicBarrier是可复用的,所有线程达到屏障点之后,CyclicBarrier会被重置。

类图结构及重要字段

public class CyclicBarrier {

    private static class Generation {
boolean broken = false;
} /** 独占锁保证同步 */
private final ReentrantLock lock = new ReentrantLock();
/** condition实现等待通知机制 */
private final Condition trip = lock.newCondition();
/** 记录线程个数 */
private final int parties;
/* 达到屏障点执行的任务 */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation(); /**
* 记录仍在等待的parties数量, 每一代count都会从初始的parties递减至0
*/
private int count; // 指定barrierAction, 在线程达到屏障后,优先执行barrierAction
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
} // 指定parties, 希望屏障拦截的线程数量
public CyclicBarrier(int parties) {
this(parties, null);
}
}
  • 基于ReentrantLock独占锁实现同步与等待通知机制,底层基于AQS。
  • int类型parties记录线程个数,表示多少线程调用await方法后,所有线程才会冲破屏障继续向下运行。
  • int类型count初始化为parties,每当有线程调用await方法就递减1,count为0表示所有线程到达屏障点。

CyclicBarrier是可复用的,因此使用两个变量记录线程个数,count变为0时,会将parties赋值给count,进行复用。

  • barrierCommand是所有线程到达屏障点后执行的任务。
  • CyclicBarrier是可复用的,Generation用于标记更新换代,generation内部的broken变量用来记录当前屏障是否被打破。

本篇文章阅读需要建立在一定独占锁,Condition条件机制的基础之上,这边推荐几篇前置文章,可以瞅一眼:

内部类Generation及相关方法

CyclicBarrier是可复用的,Generation用于标记更新换代。

    // 屏障的每一次使用都会生成一个新的Generation实例: 可能是 tripped or reset
private static class Generation {
boolean broken = false;
}

void reset()

更新换代: 首先标记一下当前这代不用了, 然后换一个新的。

    public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break掉当前的
nextGeneration(); // 开启一个新的
} finally {
lock.unlock();
}
}

void breakBarrier()

标记一下broken为true,唤醒一下await等待线程,重置count。

    private void breakBarrier() {
// 标记broken 为true
generation.broken = true;
// 重置count
count = parties;
// 唤醒因await等待的线程
trip.signalAll();
}

void nextGeneration()

唤醒一下await等待线程,重置count,更新为下一代。

    private void nextGeneration() {
// 唤醒因await等待的线程
trip.signalAll();
// 重置count,意味着下一代了
count = parties;
// 下一代了
generation = new Generation();
}

int await()

当前线程调用await方法时会阻塞,除非遇到以下几种情况:

  1. 所有线程都达到了屏障点,也就是parties个线程都调用了await()方法,使count递减至0。
  2. 其他线程调用了当前线程的interrupt()方法,中断当前线程,抛出InterruptedException而返回。
  3. 与当前屏障关联的Generation中的broken被设置为true,抛出BrokenBarrierException而返回。

它内部调用了int dowait(boolean timed, long nanos),详细解析往下面翻哈。

    public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}

int await(long timeout, TimeUnit unit)

相比于普通的await()方法,该方法增加了超时的控制,你懂的。

增加了一项:如果超时了,返回false。

    public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}

int dowait(boolean timed, long nanos)

  • 第一个参数为true,说明需要超时控制。
  • 第二个参数设置超时的时间。
    private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 与当前屏障点关联的Generation
final Generation g = generation;
// broken标志为true,则异常
if (g.broken)
throw new BrokenBarrierException();
// 如果被打断,则breakBarrier,并抛出异常
if (Thread.interrupted()) {
// 打破: 1 标记broken为true 2 重置count 3 唤醒await等待的线程
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;
// 更新: 1 唤醒await等待的线程 2 更新Generation
nextGeneration();
return 0;
} finally {
// 执行失败了,可能被打断了
if (!ranAction)
breakBarrier();
}
} // loop until tripped, broken, interrupted, or timed out
// 死循环, 结束的情况有:到达屏障点, broken了, 中断, 超时
for (;;) {
try {
// 超时控制
if (!timed)
trip.await();
else if (nanos > 0L)
// awaitNanos阻塞一段时间
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
// 标记broken为true
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();
}
}
// 正常被唤醒, 再次检查当前这一代是否已经标记了broken
if (g.broken)
throw new BrokenBarrierException();
// 最后一个线程在等待线程醒来之前,已经通过nextGeneration将generation更新
if (g != generation)
return index; if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

以parties为N为例,我们来看看这一流程。

  • 线程调用dowait方法后,首先会获取独占锁lock。如果是前N-1个线程,由于index != 0,会在条件队列中等待trip.await() or trip.awaitNanos(nanos),会相应释放锁。

  • 第N个线程调用dowait之后,此时index == 0,将会执行命令command.run(),然后调用nextGeneration()更新换代,同时唤醒所有条件队列中等待的N-1个线程。

  • 第N个线程释放锁,后续被唤醒的线程移入AQS队列,陆续获取锁,释放锁。

CyclicBarrier与CountDownLatch的区别

  • CountDownLatch基于AQS,state表示计数器的值,在构造时指定。CyclicBarrier基于ReentrantLock独占锁与Condition条件机制实现屏障逻辑。

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可复用性能够处理更为复杂【分段任务有序执行】的业务场景。

  • CyclicBarrier还提供了其他有用的方法,如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken()方法用来了解阻塞的线程是否被中断。

总结

  • CyclicBarrier = Cyclic + Barrier, 可重用 + 屏障,可以让一组线程全部到达一个屏障【同步点】,再全部冲破屏障,继续向下执行。

  • CyclicBarrier基于ReentrantLock独占锁与Condition条件机制实现屏障逻辑。

  • CyclicBarrier需要指定parties【N】以及可选的任务,当N - 1个线程调用await的时候,会在条件队列中阻塞,直到第N个线程调用await,执行指定的任务后,唤醒N - 1个等待的线程,并重置Generation,更新count。

参考阅读

  • 《Java并发编程之美》
  • 《Java并发编程的艺术》

Java并发包源码学习系列:同步组件CyclicBarrier源码解析的更多相关文章

  1. Java并发包源码学习系列:CLH同步队列及同步资源获取与释放

    目录 本篇学习目标 CLH队列的结构 资源获取 入队Node addWaiter(Node mode) 不断尝试Node enq(final Node node) boolean acquireQue ...

  2. Java并发包源码学习系列:同步组件CountDownLatch源码解析

    目录 CountDownLatch概述 使用案例与基本思路 类图与基本结构 void await() boolean await(long timeout, TimeUnit unit) void c ...

  3. Java并发包源码学习系列:同步组件Semaphore源码解析

    目录 Semaphore概述及案例学习 类图结构及重要字段 void acquire() 非公平 公平策略 void acquire(int permits) void acquireUninterr ...

  4. Java并发包源码学习系列:AQS共享式与独占式获取与释放资源的区别

    目录 Java并发包源码学习系列:AQS共享模式获取与释放资源 独占式获取资源 void acquire(int arg) boolean acquireQueued(Node, int) 独占式释放 ...

  5. Java并发包源码学习系列:ReentrantLock可重入独占锁详解

    目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...

  6. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  7. Java并发包源码学习系列:详解Condition条件队列、signal和await

    目录 Condition接口 AQS条件变量的支持之ConditionObject内部类 回顾AQS中的Node void await() 添加到条件队列 Node addConditionWaite ...

  8. Java并发包源码学习系列:挂起与唤醒线程LockSupport工具类

    目录 LockSupport概述 park与unpark相关方法 中断演示 blocker的作用 测试无blocker 测试带blocker JDK提供的demo 总结 参考阅读 系列传送门: Jav ...

  9. Java并发包源码学习系列:JDK1.8的ConcurrentHashMap源码解析

    目录 为什么要使用ConcurrentHashMap? ConcurrentHashMap的结构特点 Java8之前 Java8之后 基本常量 重要成员变量 构造方法 tableSizeFor put ...

随机推荐

  1. GeoMesa Spark

    GeoMesa Spark 一.Spark JTS 1.1 示例 1.2配置 1.3 地理空间用户定义的类型和功能 1.4 geojson输出 1.5 Building 二.Spark Core 2. ...

  2. H3C交换机端口聚合配置

    1.接入交换机: interface Ten-GigabitEthernet1/0/21 port link-mode bridge port link-type trunk port trunk p ...

  3. 服务器CPU又爆了?Linux快速排查Java程序占用CPU很高的方法

    这个问题可以说是 Java 面试的高频面试题了,有很多面试官都喜欢问这个问题,问题可能是下面这样的. 线上一台服务器 CPU 使用率100% 了,如果你碰到这样的情况,如何排查并找到问题原因? 1.场 ...

  4. bootstrap实例

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 【uva 1617】Laptop(算法效率--贪心,2种理解)

    题意:有N条长度为1的线段,要求使每条线段分别在相应区间,且"空隙"数目最小.输出"空隙"数.(1≤N≤100000) 解法:(P.S.我这题竟做了2个多小时, ...

  6. 【poj 1182】食物链(图论--带权并查集)

    题意:有3种动物A.B.C,形成一个"A吃B, B吃C,C吃A "的食物链.有一个人对N只这3类的动物有M种说法:第一种说法是"1 X Y",表示X和Y是同类. ...

  7. CF1471-C. Strange Birthday Party

    CF1471-C. Strange Birthday Party 题意: 你要举办一场生日派对.派对有\(n\)个人,每个人都有一个数字\(k_i\).超市有\(m\)件礼物,购买每件礼物需要花费\( ...

  8. 僵尸进程 & 孤儿进程

    参考博文 基本概念 僵尸进程:是所有进程都会进入的一种进程状态,子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的 PID 和 进程描述符 等资源 ...

  9. MHA 高可用介绍

    目录 MHA 介绍 MHA 简介(Master High Availability) MHA 工作原理(转载) MHA 架构 MHA 工具 Manager 节点 Node 节点 MHA 优点 MHA ...

  10. eclipse中使用debug,显示参数配置

    打开Window---->Preferences------>java------>Editor-------->Hovers 效果: