1. 是什么

可重复使用的计数器,让一堆线程互相等待,条件满足时一起往下执行

底层使用Lock+Condition实现阻塞等待和唤醒

2. 如何使用

2.1. 不带Runnable

当所有线程都到达await点的时候才一起往下执行

public class CyclicBarrierTest
{
private static final int count = 20;
private static final AtomicInteger val = new AtomicInteger();
private static final CyclicBarrier barrier = new CyclicBarrier(count); private static class CalcAction implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("1." + Thread.currentThread().getName() + "到达await点");
int result = val.incrementAndGet();
if (result % 5 == 0)
{
System.out.println("2." + Thread.currentThread().getName() + "休眠3s");
TimeUnit.SECONDS.sleep(3);
}
barrier.await();
}
catch (InterruptedException | BrokenBarrierException e)
{
e.printStackTrace();
} System.out.println("3." + Thread.currentThread().getName() + "继续执行"); } } public static void main(String[] args)
{
for (int i = 0; i < count; i++)
{
new Thread(new CalcAction()).start();
}
}
}

2.2. 带Runnale

当所有线程都到达await点的时候,最后一个到达的线程执行prepare,再一起往下执行

public class CyclicBarrierTest
{
private static final int count = 20;
private static final AtomicInteger val = new AtomicInteger();
private static final CyclicBarrier barrier = new CyclicBarrier(count, new PrepareAction()); private static class PrepareAction implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("2.所有线程到达await,最后一个到达的线程" + Thread.currentThread().getName() + "先执行PrepareAction,休眠3s");
TimeUnit.SECONDS.sleep(3);
}
catch (InterruptedException e)
{
e.printStackTrace();
} System.out.println("3.执行PrepareAction完毕"); }
} private static class CalcAction implements Runnable
{
@Override
public void run()
{
try
{
System.out.println("1." + Thread.currentThread().getName() + "到达await点");
barrier.await();
}
catch (InterruptedException | BrokenBarrierException e)
{
e.printStackTrace();
} int result = val.incrementAndGet();
System.out.println("4." + Thread.currentThread().getName() + "执行计算:result: " + result); }
} public static void main(String[] args)
{
for (int i = 0; i < count; i++)
{
new Thread(new CalcAction()).start();
}
}
}

3. 原理分析

3.1. uml

3.2. 构造方法

3.2.1. 使用Lock和Condition实现

public class CyclicBarrier {
//CyclicBarrier可以循环使用
private static class Generation {
//当前代是否损坏
boolean broken = false;
} //使用lock阻塞在await点等待
private final ReentrantLock lock = new ReentrantLock();
//使用lock.condition唤醒阻塞的所有线程往下执行
private final Condition trip = lock.newCondition();
//总共的信号量
private final int parties;
//最后一个到达的线程先执行barrierCommand,所有线程再一起继续往下执行
private final Runnable barrierCommand;
//CyclicBarrier可以重复使用,每次使用都是一个generation
private Generation generation = new Generation(); //剩下多少个线程没有到达await点
private int count; public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
//初始化总的信号量
this.parties = parties;
//初始化剩余未使用的信号量=总的信号量
//相当于一开始就加锁了parties次,那么也就需要解锁parties次
this.count = parties;
this.barrierCommand = barrierAction;
} }

3.3. await方法

public int await() throws InterruptedException, BrokenBarrierException {
try {
//调用dowait并且默认不设置超时
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
  • dowait方法
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//1.先获取锁才能往下执行
lock.lock();
try {
//当前代,每reset一次代+1
final Generation g = generation; //当前代已损坏--什么情况会导致损坏?--线程被中断,执行breakBarrier方法
if (g.broken)
throw new BrokenBarrierException(); //如果线程被中断,那么置当前代失效
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
} int index = --count;
//剩余的信号量为0,那么可以继续往下执行了
//即最后一个线程到达
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//3.最后一个到达的线程先执行Runnable
if (command != null)
command.run();
ranAction = true;
//4.唤醒所有线程继续往下执行(46行)并且换代
nextGeneration();
//5.返回,不往下执行死循环
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
} // 死循环直到超时或者信号量都用完或者中断
for (;;) {
try {
if (!timed)
//2.未设置超时,那么调用Condition.await()方法等待唤醒
trip.await();
else if (nanos > 0L)
//设置超时,那么调用Condition.await(超时)方法等待唤醒或者超时
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//发生了中断,break当前代
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else { Thread.currentThread().interrupt();
}
} //break当前代只是个标记,这里才会抛出break异常
if (g.broken)
throw new BrokenBarrierException(); //6.正常执行并且已经换代,退出循环
if (g != generation)
return index; //超时时间设置不对,直接break当前代
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//7.解锁
lock.unlock();
}
}

上面的逻辑主要分为以下几步:

  • 6行:加锁
  • 21行:减信号量
  • 43-60行:最后一个线程未到达前,其他线程阻塞等待唤醒
  • 21-40行:最后一个到达的需要执行prepareAction、唤醒线程并换代

    下面详细分析这几个步骤:

3.3.1. 首先是加锁

lock.lock();

一方面后续的所有操作必须保证是线程安全的,比如count--操作,所以需要加锁;

另一方面使用condition唤醒必须在加锁的逻辑中

3.3.2. 然后减信号量

int index = --count;

每个线程进来正常情况都会对信号量-1,减为0的时候说明所有线程准备就绪。

对于最后一个到达的线程还有特殊处理

3.3.3. 最后一个到达的需要执行prepareAction、唤醒线程并换代

if (index == 0) {  // 最后一个到达
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
//最后一个到达的线程先执行Runnable
if (command != null)
command.run();
ranAction = true;
//唤醒所有线程继续往下执行(46行)并且换代
nextGeneration();
//返回,不往下执行死循环
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
3.3.3.1. 怎么唤醒并换代的
  • nextGeneration
private void nextGeneration() {
// signal completion of last generation
//唤醒所有线程
trip.signalAll();
//换代
count = parties;
generation = new Generation();
}

3.3.4. 最后一个线程未到达前,其他线程阻塞等待唤醒

死循环直到超时或者信号量都用完或者中断
for (;;) {
try {
if (!timed)
//未设置超时,那么调用Condition.await()方法等待唤醒
trip.await();
else if (nanos > 0L)
//设置超时,那么调用Condition.await(超时)方法等待唤醒或者超时
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//发生了中断,break当前代
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else { Thread.currentThread().interrupt();
}
}
3.3.4.1. 线程被中断,置当前代失效的操作
  • breakBarrier
private void breakBarrier() {
//标志当前代损坏
generation.broken = true;
//重新计数
count = parties;
//唤醒什么??
trip.signalAll();
}

3.3.5. 最后一个线程到达唤醒其他所有线程后,其他所有线程退出循环

 // 死循环直到超时或者信号量都用完或者中断
for (;;) {
//...
if (!timed)
//2.未设置超时,那么调用Condition.await()方法等待唤醒
trip.await(); //... //6.正常执行并且已经换代,退出循环
if (g != generation)
return index; //... }

4. 总结

  • 让一堆线程互相等待,条件满足(信号量降为0)时最后一个到达的线程先执行Runnable,接着所有线程一起往下执行

  • 调用await的时候

    • 除了最后一个线程外,其他所有线程依次获取lock,对信号量-1,阻塞等待唤醒(加入condition队列并释放锁)
    • 最后一个线程到达后执行Runnable,唤醒所有线程(把所有condition队列中的节点转到AQS中)
    • 唤醒的所有线程依次抢占到锁(从AQS队列中移除)后往下执行,检查代后退出循环,解锁

5. 参考

Java源码分析系列笔记-8.CyclicBarrier的更多相关文章

  1. Java源码分析系列之HttpServletRequest源码分析

    从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...

  2. Java源码分析系列

    1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...

  3. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  4. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  5. [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat

    概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...

  6. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  7. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  8. spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析

    更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...

  9. spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...

  10. spring源码分析系列 (3) spring拓展接口InstantiationAwareBeanPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.InstantiationAwareBeanPostProcessor简述与demo示例 二.InstantiationAwareBean ...

随机推荐

  1. TreeSet的add方法源码分析

    一.JDK 1.8 中 TreeSet 的 add 方法源码详细分析 TreeSet 是 Java 集合框架中的一个有序集合类,基于红黑树(TreeMap)实现.TreeSet 的 add 方法用于向 ...

  2. 第十五届蓝桥杯javaA组 砍柴 (两种写法)详解

    参考资料 原题链接砍柴 - 蓝桥云课 (lanqiao.cn) 区间质数搜索--埃拉托斯特尼筛法和欧拉筛法-CSDN博客 思路 质数筛 + 二分 + 博弈 + 状态机(只因bushi) $$ 状态转移 ...

  3. Asp.net mvc基础(二)Controller给View传递数据的方式

    1.ViewData传值 步骤一:通过在控制器中以键值对的形式进行赋值 ViewData["键"] = 值 赋值: 调用: 2.ViewBag传值 ViewBag是dynamic类 ...

  4. kettle介绍-Step之Return steps metrics

    Return steps metrics转换步骤信息统计介绍 转换步骤信息统计步骤可以用于统计当前转换中的其它步骤信息,包括步骤执行后的输入行数.输入行数.读入行数.更新行数等.此步骤可以直接拖入转换 ...

  5. 微信小程序 6/12 的坑

    配置 小程序的时候配置请求的是 https://xxx 不是http://xxx 前端请求的链接都是https

  6. Spring基于XML AOP事务控制

    Spring基于XML AOP事务控制 源码 代码测试 pom.xml <?xml version="1.0" encoding="UTF-8"?> ...

  7. Flutter集成微信小程序技术教程

    .markdown-body { color: rgba(89, 89, 89, 1); font-size: 15px; font-family: -apple-system, system-ui, ...

  8. python ast模块使用

    ast(Abstract Syntax Trees)是python中非常有用的一个模块,我们可以通过分析python的抽象语法树来对python的代码进行分析和修改. ast作用在python代码的语 ...

  9. STM32在使用Clion平台开发时调试失败 SystemClock_Config 返回 HAL_ERROR

    问题记录 在尝试使用Clion在STM32平台上开发调试时,需要通过OpenOCD结合ST-Link等调试器进行烧录和调试.但通过STM32CubeMX生成代码后,发现出现以下现象: 程序能够正常编译 ...

  10. 阅读类元服务开发笔记---week4

    .markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...