Aqs的CyclicBarrier。

news2024/11/27 4:22:35

今天我们来学习AQS家族的“外门弟子”:CyclicBarrier。

为什么说CyclicBarrier是AQS家族的“外门弟子”呢?那是因为CyclicBarrier自身和内部类Generation并没有继承AQS,但在源码的实现中却深度依赖AQS家族的成员ReentrantLock。就像修仙小说中,大家族会区分外门和内门,外门弟子通常会借助内门弟子的名声行事,CyclicBarrier正是这样,因此算是AQS家族的“外门弟子”。在实际的面试中,CyclicBarrier的出现的次数较少,通常会出现在与CountDownLatch比较的问题当中

今天我们就逐步拆解CyclicBarrier,来看看它与CountDownLatch之间到底有什么差别。

CyclicBarrier是什么?

先从CyclicBarrier的名字开始入手,Cyclic是形容词,译为“循环的,周期的”,Barrier是名词,译为“屏障,栅栏”,组合起来就是“循环的屏障”,那么该怎么理解“循环的屏障”呢?我们来看CyclicBarrier的注释是怎么解释的:

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.   CyclicBarrier是一种同步辅助工具,允许一组线程等待彼此到达共同的屏障点。

The barrier is called cyclic because it can be re-used after the waiting threads are released. 因为在等待线程释放后可以重复使用,所以屏障被称为循环屏障。

看起来与CountDownLatch有些相似,我们通过一张图来展示下CyclicBarrier是怎样工作的:

部分线程到达屏障后,会在屏障处等待,只有全部线程都到达屏障后,才会继续执行。如果以CountDownLatch中越野徒步来举例的话,把老板拿掉,选手之间的互相等待,就是CyclicBarrier了。

另外,注释中说CyclicBarrier是“re-used”,即可重复使用的。回想一下CountDownLatch的实现,并未做任何重置计数器的工作,即当CountDownLatch的计数减为0后不能恢复,也就是说CountDownLatch的功能是一次性的

Tips:实际上,可以用CountDownLatch实现类似于CyclicBarrier的功能。

CyclicBarrier怎么用?

我们用没有老板参加的越野徒步来举例,部分先到的选手要等待后到的选手一起吃午饭,用CyclicBarrier来实现的代码是这样的:

// 初始化CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(10);

for (int i = 0; i < 10; i++) {
	int finalI = i;
	new Thread(() -> {
		try {
			TimeUnit.SECONDS.sleep((finalI + 1));
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		try {
			System.out.println("选手[" + finalI + "]到达终点,等待其他选手!!!");

			// 线程在屏障点处等待
			cyclicBarrier.await();

			System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
		} catch (InterruptedException | BrokenBarrierException e) {
			throw new RuntimeException(e);
		}
	}).start();
}

用法和CountDownLatch很相似,构造函数设置CyclicBarrier需要多少个线程达到屏障后统一行动,区别是CyclicBarrier在每个线程中都调用了CyclicBarrier#await,而我们在使用CountDownLatch时只在主线程中调用了一次CountDownLatch#await

那CountDownLatch可以在线程中调用CountDownLatch#await吗?答案是可以的,这样使用的效果和CyclicBarrier是一样的:

CountDownLatch countDownLatch = new CountDownLatch(10);

for (int i = 0; i < 10; i++) {
	int finalI = i;
	new Thread(() -> {
		try {
			TimeUnit.SECONDS.sleep((finalI + 1));
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		System.out.println("选手[" + finalI + "]到达终点!!!");
		countDownLatch.countDown();
		try {
			countDownLatch.await();
			System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}).start();
}

通过上面的例子,我们不难想到CyclicBarrier#await方法是同时具备了CountDownLatch#countDown方法和CountDownLatch#await方法的能力,即执行了计数减1,又执行了暂停线程

CyclicBarrier是怎么实现的?

我们先整体认识一下CyclicBarrier:

CyclicBarrier的内部结构比CountDownLatch复杂一些,除了我们前面提到的借助AQS的“内门弟子”ReentrantLock类型的lock和Condition类型的trip外,CyclicBarrier还有两个“特别”的地方:

  • 内部类Generation,直译过来是“代”,它起到什么作用?
  • Runnable类型的成员变量barrierCommand,它又做了些什么?

其余的部分,大部分可以在CountDownLatch中找到对应的方法,或者通过名称我们就很容易得知它们的作用。

CyclicBarrier的构造方法

CyclicBarrier提供了两个(实际是一个)构造方法:

// 需要到达屏障的线程数
private final int parties;

// 所有线程都到达后执行的动作
private final Runnable barrierCommand;

// 计数器
private int count;

public CyclicBarrier(int parties) {
this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
	if (parties <= 0) {
		throw new IllegalArgumentException();
	}
	this.parties = parties;
	this.count = parties;
	this.barrierCommand = barrierAction;
}

第二个构造函数接收了两个参数:

  • parties:表示需要多少个线程到达屏障处调用CyclicBarrier#await
  • barrierAction:所有线程到达屏障后执行的动作。

构造方法的代码一如既往的简单,只有一处比较容易产生疑惑,parties和count有什么区别?

首先来看成员变量的声明,parties使用了final,表明它是不可变的对象,代表CyclicBarrier需要几个线程共同到达屏障处;而count是计数器,初始值是parties,随着到达屏障处的线程数量增多count会逐步减少至0。

CyclicBarrier的内部类Generation

private static class Generation {
	Generation() {}  

	boolean broken;
}

Generation用于标记CyclicBarrier的当前代,Doug Lea是这么解释它的作用的:

Each use of the barrier is represented as a generation instance. The generation changes whenever the barrier is tripped, or is reset.

每次使用屏障(CyclicBarrier)都需要一个Generation实例。无论是通过屏障还是重置屏障,Generation都会发生改变。

Generation中的broken用于标记当前的CyclicBarrier是否被打破,默认为false,值为true时表示当前CyclicBarrier已经被打破,此时CyclicBarrier不能正常使用,需要调用CyclicBarrier#reset方法重置CyclicBarrier的状态。

CyclicBarrier#await方法

前面我们猜测CyclicBarrier#await方法即实现了计数减1,又实现了线程等待的功能,下面我们就通过源码来验证我们的想法:

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

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

两个重载方法都指向了CyclicBarrier#dowait方法:

private int dowait(boolean timed, long nanos)  throws InterruptedException, BrokenBarrierException, TimeoutException {
	// 使用ReentrantLock
	final ReentrantLock lock = this.lock;
	lock.lock();

	try {
		// 第2部分
		// 获取CyclicBarrier的当前代,并检查CyclicBarrier是否被打破
		final Generation g = generation;
		if (g.broken) {
			throw new BrokenBarrierException();
		}

		// 线程被中断时,调用breakBarrier方法
		if (Thread.interrupted()) {
			breakBarrier();
			throw new InterruptedException();
		}

		// 第3部分
		//计数器减1
		int index = --count;
		// 计数器为0时表示所有线程都到达了,此时要做的就是唤醒等待中的线程
		if (index == 0) {
			boolean ranAction = false;
			try {
				// 执行唤醒前的操作
				final Runnable command = barrierCommand;
				if (command != null) {
					command.run();
				}
				ranAction = true;
				// CyclicBarrier进入下一代
				nextGeneration();
				return 0;
			} finally {
				if (!ranAction) {
					breakBarrier();
				}
			}
		}

		// 第4部分
		// 只有部分线程到达屏障处的情况
		for (;;) {
			try {
				//调用等待逻辑)
				if (!timed) {
					trip.await();
				} else if (nanos > 0L) {
					nanos = trip.awaitNanos(nanos);
				}
			} catch (InterruptedException ie) {
				// 线程被中断时,调用breakBarrier方法
				if (g == generation && ! g.broken) {
					breakBarrier();
					throw ie;
				} else {
					Thread.currentThread().interrupt();
				}
			}
			if (g.broken) {
				throw new BrokenBarrierException();
			}
			// 如果不是当前代,返回计数器的值
			if (g != generation) {
				return index;
			}
			// 如果等待超时,调用breakBarrier方法
			if (timed && nanos <= 0L) {
				breakBarrier();
				throw new TimeoutException();
			}
		}
	} finally {
		lock.unlock();
	}
}

CyclicBarrier#dowait方法看起来很长,但如果拆成3部分来看逻辑并不复杂:

  • 第1部分:CyclicBarrier与线程的状态校验;
  • 第2部分:当计数器减1后值为0时,唤醒所有等待中的线程;
  • 第3部分:当计数器减1后值不为0时,线程进入等待状态。

先来看第1部分,CyclicBarrier与线程的状态校验的部分,先是判断CyclicBarrier是否被打破,接着判断当前线程是否为中断状态,如果是则调用CyclicBarrier#breakBarrier方法:

private void breakBarrier() {
	generation.broken = true;
	count = parties;
	trip.signalAll();
}

CyclicBarrier#breakBarrier方法非常简单,只做了3件事:

  • 标记CyclicBarrier被打破;
  • 重置CyclicBarrier的计数器;
  • 唤醒全部等待中的线程。

也就是说,一旦有个线程标记为中断状态,都会直接打破CyclicBarrier的屏障。

我们先跳过第2部分的唤醒逻辑,直接来看第3部分线程进入等待状态的逻辑。根据timed参数选择调用Condition不同的等待方法,随后是对异常的处理和线程中断状态的处理,同样是调用CyclicBarrier#breakBarrier,标记CyclicBarrier不可用。线程进入等待状态的逻辑并不复杂,本质上是通过AQS的Condition来实现的。

最后来看第2部分唤醒所有等待中线程的操作,根据计数器是否为0判断是否需要进行唤醒。如果需要唤醒,最后一个执行CyclicBarrier#await的线程执行barrierCommand(此时尚未执行任何线程唤醒的操作),做通过屏障前的处理操作,接着调用CyclicBarrier#nextGeneration方法:

private void nextGeneration() {
	trip.signalAll();
	count = parties;
	generation = new Generation();
}

CyclicBarrier#nextGeneration方法也做了3件事:

  • 唤醒所有Condition上等待的线程;
  • 重置CyclicBarrier的计数器;
  • 创建新的Generation对象。

很符合进入“下一代”的名字,先唤醒“上一代”所有等待中的线程,然后重置CyclicBarrier的计数器,最后更新CyclicBarrier的Generation对象,对CyclicBarrier进行重置工作,让CyclicBarrier进入下一个纪元。

到这里我们不难发现,CyclicBarrier自身只做了维护计数器和重置计数器的工作,而保证互斥性和线程的等待与唤醒则是依赖AQS家族的成员完成的:

  • ReentrantLock保证了同一时间只有一个线程可以执行CyclicBarrier#await,即同一时间只有一个线程可以维护计数器;
  • Condition为CyclicBarrier提供了条件等待队列,完成了线程的等待与唤醒的工作。

CyclicBarrier#reset方法

最后我们来看CyclicBarrier#reset方法:

public void reset() {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		// 主动打破CyclicBarrier
		breakBarrier();
		// 使CyclicBarrier进入下一代
		nextGeneration();
	} finally {
		lock.unlock();
	}
}

CyclicBarrier#reset方法都是老面孔,先是CyclicBarrier#breakBarrier打破上一代CyclicBarrier,既然要重新开始就不要再“怀念”过去了;最后调用CyclicBarrier#nextGeneration开始新的时代。需要注意的是,这里加锁的目的是为了保证执行CyclicBarrier#reset时,没有任何线程正在执行CyclicBarrier#await方法。

好了,到这里CyclicBarrier的核心内容我们就一起分析完了,剩下的方法就非常简单了,相信通过名字大家就可以了解它们的作用,并猜到它们的实现了。

TipsCyclicBarrier#getNumberWaiting中加了锁,这是为什么?

CountDownLatch和Cyclicbarrier有什么区别?

最后的部分,我们来解答下开篇时的面试题,CountDownLatch和Cyclicbarrier有什么区别?

第1点:CyclicBarrier可以重复使用,CountDownLatch不能重复使用

无论是正常使用结束,还是调用CyclicBarrier#reset方法,Cyclicbarrier都可以重置内部的计数器

第2点:Cyclicbarrier只阻塞调用CyclicBarrier#await方法的线程,而CountDownLatch可以阻塞任意一个或多个线程

CountDownLatch将计数减1与阻塞拆分成了CountDownLatch#countDownCountDownLatch#await两个方法,而Cyclicbarrier只通过CyclicBarrier#await完成两步操作。如果在同一个线程中连续CountDownLatch#countDownCountDownLatch#await则实现了与CyclicBarrier#await方法相同的功能。


如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/969174.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java 复习笔记 - 学生管理系统篇

文章目录 学生管理系统一&#xff0c;需求部分需求分析初始菜单学生类添加功能删除功能修改功能查询功能 二&#xff0c;实现部分&#xff08;一&#xff09;初始化主界面&#xff08;二&#xff09;编写学生类&#xff08;三&#xff09;编写添加学生方法&#xff08;四&#…

ref 操作 React 定时器

秒表 需要将 interval ID 保存在 ref 中&#xff0c;以便在需要时能够清除计时器。 import { useRef, useState } from "react";const SecondWatch () > {const [startTime, setStartTime] useState<any>(null);const [now, setNow] useState<any>…

Elasticsearch中RestClient使用

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

如何将自己的镜像使用 helm 部署

本文分别从如下几个方面来分享一波 如何将自己的镜像使用 helm 部署 简单介绍一下 helm 使用自己写 yaml 文件的方式在 k8s 中部署应用 使用 helm 的方式在 k8s 中部署应用 简单介绍一下 helm Helm 是 Kubernetes 的包管理器&#xff0c;在云原生领域用于应用打包和分发 Hel…

12. 微积分 - 梯度积分

Hi,大家好。我是茶桁。 上一节课,我们讲了方向导数,并且在最后留了个小尾巴,是什么呢?就是梯度。 我们再来回看一下但是的这个式子: [ f x f y

信息系统项目管理师(第四版)教材精读思维导图-第八章项目整合管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 目录 8.1 管理基础 8.2 管理过程 8.3 制定项…

LRU算法 vs Redis近似LRU算法

LRU(Least Recently Use)算法&#xff0c;是用来判断一批数据中&#xff0c;最近最少使用算法。它底层数据结构由Hash和链表结合实现&#xff0c;使用Hash是为了保障查询效率为O(1)&#xff0c;使用链表保障删除元素效率为O(1)。 LRU算法是用来判断最近最少使用到元素&#xf…

最短路Dijkstra,spfa,图论二分图算法AYIT---ACM训练(模板版)

文章目录 前言A - Dijkstra Algorithm0x00 算法题目0x01 算法思路0x02 代码实现 B - 最长路0x00 算法题目0x01 算法思路0x02 代码实现 C - 二分图最大匹配0x00 算法题目0x01 算法思路0x02 代码实现 D - 搭配飞行员0x00 算法题目0x01 算法思路0x02 代码实现 E - The Perfect Sta…

企业架构LNMP学习笔记11

Nginx配置文件的介绍&#xff1a; #nginx子进程启动用户 #user nobody; #子进程数量 一般调整为cpu核数或者倍数 worker_processes 1; #错误日志定义 #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info;#进程pid 存储文件…

ISO/IEC/ITU标准如何快速查找(三十九)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只有行动才是治疗恐惧和懒惰的唯一良药. 更多原创,欢迎关注:Android…

C++中的语法知识虚继承和虚基类

多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。 多继承时很容易产生命名冲突,即使我们很小心地将所有类…

UDP和TCP协议报文格式详解

在初识网络原理(初识网络原理_蜡笔小心眼子&#xff01;的博客-CSDN博客)这篇博客中,我们简单的了解了一下TCP/IP五层网络模型,这篇博客将详细的学习一下五层网络模型中传输层的两个著名协议:UDP和TCP 目录 一, 传输层的作用 二, UDP 1,UDP协议的特点 2,UDP报文格式 三, TC…

【数据结构】如何设计循环队列?图文解析(LeetCode)

LeetCode链接&#xff1a;622. 设计循环队列 - 力扣&#xff08;LeetCode&#xff09; 目录 做题思路 只开辟 k 个空间 多开一个空间 代码实现 1. 循环队列的结构 2. 开辟空间 3. 判断空 4. 判断满 5. 队尾插入数据 6. 队头删除数据 7. 获取队头元素 8. 获取队尾元…

ElasticSearch第二讲:ES详解 - ElasticSearch基础概念

ElasticSearch第二讲&#xff1a;ES详解 - ElasticSearch基础概念 在学习ElasticSearch之前&#xff0c;先简单了解下ES流行度&#xff0c;使用背景&#xff0c;以及相关概念等。本文是ElasticSearch第二讲&#xff0c;ElasticSearch的基础概念。 文章目录 ElasticSearch第二讲…

【GoldenDict】win11牛津高阶英汉双解词典安装使用方法

【词典资源】 1&#xff08;本文章使用的版本&#xff09;牛津高阶&#xff08;第10版 英汉双解&#xff09; V11.8&#xff1a; https://pan.baidu.com/s/11272Cldde_2UttQkWS2MlQ 提取码&#xff1a;0p3j 2&#xff08;另一版本&#xff09;第十版 v13.2&#xff1a; ht…

信息系统项目管理师(第四版)教材精读思维导图-第九章项目范围管理

请参阅我的另一篇文章&#xff0c;综合介绍软考高项&#xff1a; 信息系统项目管理师&#xff08;软考高项&#xff09;备考总结_计算机技术与软件专业技术_铭记北宸的博客-CSDN博客 本章思维导图PDF格式 本章思维导图XMind源文件 目录 9.1 管理基础 9.2 管理过程 9.3 规划范…

【Linux】线程安全-信号量

文章目录 信号量原理信号量保证同步和互斥的原理探究信号量相关函数初始化信号量函数等待信号量函数释放信号量函数销毁信号量函数 信号量实现生产者消费者模型 信号量原理 信号量的原理&#xff1a;资源计数器 PCB等待队列 函数接口 资源计数器&#xff1a;对共享资源的计…

Python之单调栈

单调栈 了解单调栈先要了解栈。栈&#xff08;stack&#xff09;又名堆栈&#xff0c;它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶&#xff0c;相对地&#xff0c;把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈&…

用JAVA(springboot) 开发的彩票模拟系统

闲暇时间&#xff0c;自己写了一个模拟彩票系统&#xff0c;里面研究了开奖算法&#xff0c;下单算法&#xff0c;彩票的各种计算规则。需要源码的私信&#xff0c;研究为主&#xff0c;切勿商务用途。

2022年06月 C/C++(七级)真题解析#中国电子学会#全国青少年软件编程等级考试

C/C编程&#xff08;1~8级&#xff09;全部真题・点这里 第1题&#xff1a;有多少种二叉树 输入n(1<n<13)&#xff0c;求n个结点的二叉树有多少种形态 时间限制&#xff1a;1000 内存限制&#xff1a;65536 输入 整数n 输出 答案 样例输入 3 样例输出 5 这个问题可以使用…