回环屏障CyclicBarrier原理探究

上节介绍的CountDownLatch在解决多个线程同步方面相对于调用线程的join方法已经有了不少优化,但是CountDownLatch的计数器是一次性的,也就是等到计数器值变为0后,再调用CountDownLatch的await和countdown方法都会立刻返回,这就起不到线程同步的效果了。

所以为了满足计数器可以重置的需要,JDK开发组提供了CyclicBarrier类,并且CyclicBarrier类的功能并不限于CountDownLatch的功能。

从字面意思理解,CyclicBarrier是回环屏障的意思,它可以让一组线程全部达到一个状态后再全部同时执行。

这里之所以叫作回环是因为当所有等待线程执行完毕,并重置CyclicBarrier的状态后它可以被重用。

之所以叫作屏障是因为线程调用await方法后就会被阻塞,这个阻塞点就称为屏障点,等所有线程都调用了await方法后,线程们就会冲破屏障,继续向下运行。

案例介绍

在介绍原理前先介绍几个实例以便加深理解。在下面的例子中,我们要实现的是,使用两个线程去执行一个被分解的任务A,当两个线程把自己的任务都执行完毕后再对它们的结果进行汇总处理。

在这里插入图片描述
输出结果如下
在这里插入图片描述
如上代码创建了一个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来完成这个需求。

在这里插入图片描述
输出结果如下。
在这里插入图片描述
在如上代码中,每个子线程在执行完阶段1后都调用了await方法,等到所有线程都到达屏障点后才会一块往下执行,这就保证了所有线程都完成了阶段1后才会开始执行阶段2。

然后在阶段2后面调用了await方法,这保证了所有线程都完成了阶段2后,才能开始阶段3的执行。

这个功能使用单个CountDownLatch是无法完成的。

实现原理探究

为了能够一览CyclicBarrier的架构设计,下面先看下CyclicBarrier的类图结构,如图所示。

在这里插入图片描述
由以上类图可知,CyclicBarrier基于独占锁实现,本质底层还是基于AQS的。

parties用来记录线程个数,这里表示多少线程调用await后,所有线程才会冲破屏障继续往下运行。

而count一开始等于parties,每当有线程调用await方法就递减1,当count为0时就表示所有线程都到了屏障点。

你可能会疑惑,为何维护parties和count两个变量,只使用count不就可以了?别忘了CycleBarier是可以被复用的,使用两个变量的原因是,parties始终用来记录总的线程个数,当count计数器值变为0后,会将parties的值赋给count,从而进行复用。

使用lock首先保证了更新计数器count的原子性。

另外使用lock的条件变量trip支持线程间使用await和signal操作进行同步。

最后,在变量generation内部有一个变量broken,其用来记录当前屏障是否被打破。

注意,这里的broken并没有被声明为volatile的,因为是在锁内使用变量,所以不需要声明。

在这里插入图片描述

int await()方法

当前线程调用CyclicBarrier的该方法时会被阻塞,直到满足下面条件之一才会返回:parties个线程都调用了await方法,也就是线程都到了屏障点。

其他线程调用了当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InterruptedException异常而返回。

与当前屏障点关联的Generation对象的broken标志被设置为true时,会抛出BrokenBarrierException异常,然后返回。

由如下代码可知,在内部调用了dowait方法。第一个参数为false则说明不设置超时时间,这时候第二个参数没有意义。

在这里插入图片描述

boolean await(long timeout,TimeUnit unit)方法

当前线程调用CyclicBarrier的该方法时会被阻塞,直到满足下面条件之一才会返回:parties个线程都调用了await()方法,也就是线程都到了屏障点,这时候返回true。

设置的超时时间到了后返回false;其他线程调用当前线程的interrupt()方法中断了当前线程,则当前线程会抛出InterruptedException异常然后返回;与当前屏障点关联的Generation对象的broken标志被设置为true时,会抛出BrokenBarrierException异常,然后返回。

由如下代码可知,在内部调用了dowait方法。第一个参数为true则说明设置了超时时间,这时候第二个参数是超时时间。

在这里插入图片描述

int dowait(boolean timed,long nanos)方法

该方法实现了CyclicBarrier的核心功能,其代码如下。

在这里插入图片描述
以上是dowait方法的主干代码。

当一个线程调用了dowait方法后,首先会获取独占锁lock,如果创建CycleBarrier时传递的参数为10,那么后面9个调用线程会被阻塞。

然后当前获取到锁的线程会对计数器count进行递减操作,递减后count=index=9,因为index!=0所以当前线程会执行代码(4)。

如果当前线程调用的是无参数的await()方法,则这里timed=false,所以当前线程会被放入条件变量trip的条件阻塞队列,当前线程会被挂起并释放获取的lock锁。

如果调用的是有参数的await方法则timed=true,然后当前线程也会被放入条件变量的条件队列并释放锁资源,不同的是当前线程会在指定时间超时后自动被激活。

当第一个获取锁的线程由于被阻塞释放锁后,被阻塞的9个线程中有一个会竞争到lock锁,然后执行与第一个线程同样的操作,直到最后一个线程获取到lock锁,此时已经有9个线程被放入了条件变量trip的条件队列里面。

最后count=index等于0,所以执行代码(2),如果创建CyclicBarrier时传递了任务,则在其他线程被唤醒前先执行任务,任务执行完毕后再执行代码(3),唤醒其他9个线程,并重置CyclicBarrier,然后这10个线程就可以继续向下运行了。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

视图与索引连表查询内/外联和子查询

1.视图 先介绍一下视图: 从SQL的角度来看,视图和表是相同的,两者的区别在于表中存储的是实际的数据,而视图中保存的是SELECT语句(视图本身并不存储数据)。 使用视图可以轻松完成跨多表查询数据等复杂操作…

解析工会排队:动静奖励结合的魅力

每天五分钟讲解一个商业模式知识,大家好我是模式策划啊浩Zeropan_HH。 数字时代数字思想,当你还在苦恼如何让自己的商业城堡扩大时,不如放空思想来看看啊浩的文章,或许可以给你一些启发。今天的给大家分享的模式来源于《微三云赢…

Project软件使用指南:六个关键功能助力项目成功

在项目管理的复杂世界中,Project软件提供了关键的解决方案。主要功能包括:1、任务和进度管理、2、资源分配、3、财务监控、4、风险评估、5、协作增强、6、报告和洞察力。特别是在任务和进度管理方面,Project软件通过动态时间表和任务跟踪工具…

休息一会 sleep

文章目录 休息一会 sleep休息5分钟1小时后提醒我时分秒搭配使用倒计时计时器结合脚本更多信息 休息一会 sleep … note:: 莫听穿林打叶声,何妨吟啸且徐行。 苏轼 Linux sleep命令可以用来将目前动作延迟一段时间。 sleep的官方定义为: sleep - delay …

React之自定义路由组件

开篇 react router功能很强大,可以根据路径配置对应容器组件。做到组件的局部刷新,接下来我会基于react实现一个简单的路由组件。 代码 自定义路由组件 import {useEffect, useState} from "react"; import React from react // 路由配置 e…

YOLOv8 Ultralytics:使用Ultralytics框架进行定向边界框对象检测

YOLOv8 Ultralytics:使用Ultralytics框架进行定向边界框对象检测 前言相关介绍前提条件实验环境安装环境项目地址LinuxWindows 使用Ultralytics框架进行定向边界框对象检测参考文献 前言 由于本人水平有限,难免出现错漏,敬请批评改正。更多精…

Kali Linux——aircrack-ng无线教程

目录 一、准备 二、案例 1、连接usb无线网卡 2、查看网卡信息 3、开启网卡监听 4、扫描wifi信号 5、抓取握手包 6、强制断开连接 7、破解握手包 三、预防 一、准备 1、usb无线网卡(笔记本也是需要用到) 2、密码字典(Kali 系统自带…

好用便签:如何高效完成待办事项,提高工作效率?

在职场上,很多打工人总会有各种各样的待办事项需要处理,有时候因为手头上正在做的事还没做完,又接到一些其他的任务,导致不知道先做哪个,或者是忘了做某件事,导致工作效率极低。那么,如何高效处…

FineBI实战项目一(15):订单销售总额分析开发

点击新建组件,创建订单销售总额组件。 选择自定义图表,选择文本,拖拽要分析的字段到文本中。 进入仪表板,拖拽刚刚的组件进入仪表板,然后在再编辑标题。 效果如下

FineBI实战项目一(18):每小时上架商品个数分析开发

点击新建组件,创建每小时上架商品个数组件。 选择线图,拖拽cnt(总数)到纵轴,拖拽hourStr到横轴。 修改横轴和纵轴的文字。 调节连线样式。 添加组件到仪表板。

手写一个starter来理解SpringBoot的自动装配

自动装配以及简单的解析源码 自动装配是指SpringBoot在启动的时候会自动的将系统中所需要的依赖注入进Spring容器中 我们可以点开SpringBootApplication这个注解来一探究竟 点开这个注解可以发现这些 我们点开SpringBootConfiguration这个注解 可以发现实际上SpringBootApp…

C# 使用Fleck创建WebSocket服务器

目录 写在前面 代码实现 服务端代码 客户端代码 调用示例 写在前面 Fleck 是 C# 实现的 WebSocket 服务器,通过 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道;两者之间…