非阻塞同步算法实战(二)-BoundlessCyclicBarrier
本人是本文的作者,首发于ifeve(非阻塞同步算法实战(二)-BoundlessCyclicBarrier)
前言
相比上一 篇而言,本文不需要太多的准备知识,但技巧性更强一些。因为分析、设计的过程比较复杂繁琐,也限于篇幅,所以,主要展示如何解决这些需求,和讲解代码。另外,所讲的内容也是后一篇实战中需要用到的一个工具类。
注:该工具类已分享到https://github.com/trytocatch/concurrent-tools
需求介绍
我需要编写一个同步工具,它需要提供这样几个方法:await、pass、cancel。某个线程调用await时,会被阻塞;当调用pass方法时,之前因为await而阻塞的线程将全部被解除阻塞,之后调用await的线程继续被阻塞,直到下一次调用pass。
该工具同时还维护一个版本号,await方法可以带一个目标版本号,如果当前的版本号比目标版本号新或相同,则直接通过,否则,阻塞本线程,直到到达或超过目标版本。调用pass的时候,更新版本号。
如果停止了版本更新,可使用cancel方法来解除所有因await而阻塞的线程,包括指定版本号的。此方法用于避免无谓地等待。若await发生在cancel之后,则仍将被阻塞。
因为CountDownLatch不允许重复使用,CyclicBarrier只支持固定个数的线程,并且都没有维护一个版本号,所以没有已有的类能实现上面的需求,需要自己实现。
问题分析
简单分析可知,应该维护一个队列,来保存当前被阻塞的线程,用于在pass时对它们一一解除阻塞,pass时应该使用一个新的队列,否则不方便正确处理pass前和pass后调用await的线程。
至此,问题的关键就明了了:如何将队列的替换和版本号的更新这两个操作做成原子的。
解决方案
以前在《JAVA并发编程实践》曾看到过这样一个小技巧,如果要原子地更新两个变量,那么可以创建一个新的类将它们封装起来,将这两个变量当定义成类成员变量,更新时,用CAS更新这个类的引用即可。
因为较为复杂,下面先给出完整的代码,再讲解其中的关键。
注意:上面所说pass,在代码中的具体实现为nextCycle,有两个版本,一个自动维护版本号,一个由调用者维护版本号。
/**
* @author trytocatch@163.com
* @time 2013-1-31
*/
public class BoundlessCyclicBarrier {
protected final AtomicReference<VersionQueue> waitQueueRef; public BoundlessCyclicBarrier() {
this(0);
} public BoundlessCyclicBarrier(int startVersion) {
waitQueueRef = new AtomicReference<VersionQueue>(new VersionQueue(startVersion));
} public final void awaitWithAssignedVersion(int myVersion)
throws InterruptedException {
awaitImpl(true, myVersion, 0);
} /**
*
* @param myVersion
* @param nanosTimeout
* @return if timeout, or be canceled and doesn't reach myVersion, returns false
* @throws InterruptedException
*/
public final boolean awaitWithAssignedVersion(int myVersion, long nanosTimeout) throws InterruptedException {
return awaitImpl(true, myVersion, nanosTimeout);
} public final void await() throws InterruptedException {
awaitImpl(false, 0, 0);
} /**
*
* @param nanosTimeout
* @return if and only if timeout, returns false
* @throws InterruptedException
*/
public final boolean await(long nanosTimeout)
throws InterruptedException {
return awaitImpl(false, 0, nanosTimeout);
} /**
* pass and version++(some threads may not be unparked when awaitImpl is in process, but it's OK in this Barrier)
* @return old queue version
*/
public int nextCycle() {
VersionQueue oldQueue = waitQueueRef.get();
VersionQueue newQueue = new VersionQueue(oldQueue.version + 1);
for(;;){
if (waitQueueRef.compareAndSet(oldQueue, newQueue)) {
for (Thread t : oldQueue.queue)
LockSupport.unpark(t);
break;
}
oldQueue = waitQueueRef.get();
newQueue.version = oldQueue.version + 1;
}
return oldQueue.version;
} /**
* pass and assign the next cycle version(caller should make sure that the newAssignVersion is right)
* @param newAssignVersion
*/
public void nextCycle(int newAssignVersion) {
VersionQueue oldQueue = waitQueueRef.getAndSet(new VersionQueue(newAssignVersion));
for (Thread t : oldQueue.queue)
LockSupport.unpark(t);
} /**
* if version update has stopped, invoke this to awake all threads
*/
public void cancel(){
VersionQueue oldQueue = waitQueueRef.get();
if (waitQueueRef.compareAndSet(oldQueue, new VersionQueue(oldQueue.version, true))) {
for (Thread t : oldQueue.queue)
LockSupport.unpark(t);
} public final int getVersion() {
return waitQueueRef.get().version;
} private static final class VersionQueue {
final private ConcurrentLinkedQueue queue;
int version;
final boolean isCancelQueue; VersionQueue(int curVersion){
this(curVersion, false);
} VersionQueue(int curVersion, boolean isCancelQueue) {
this.version = curVersion;
this.isCancelQueue = isCancelQueue;
queue = new ConcurrentLinkedQueue();
}
} /**
*
* @param assignVersion is myVersion available
* @param myVersion wait for this version
* @param nanosTimeout wait time(nanosTimeout <=0 means that nanosTimeout is invalid)
* @return if timeout, or be canceled and doesn't reach myVersion, returns false
* @throws InterruptedException
*/
protected boolean awaitImpl(boolean assignVersion, int myVersion,
long nanosTimeout) throws InterruptedException {
boolean timeOutEnable = nanosTimeout > 0;
long lastTime = System.nanoTime();
VersionQueue newQueue = waitQueueRef.get();//A
if (assignVersion && newQueue.version - myVersion >= 0)
return true;
while (true) {
VersionQueue submitQueue = newQueue;//B
submitQueue.queue.add(Thread.currentThread());//C
while (true) {
newQueue = waitQueueRef.get();//D
if (newQueue != submitQueue){//E: it's a new cycle
if(assignVersion == false)
return true;
else if(newQueue.version - myVersion >= 0)
return true;
else if (newQueue.isCancelQueue)//F: be canceled
return false;
else//just like invoking awaitImpl again
break;
}
if (timeOutEnable) {
if (nanosTimeout <= 0)
return false;
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
nanosTimeout -= now - lastTime;
lastTime = now;
} else
LockSupport.park(this);
if (Thread.interrupted())
throw new InterruptedException();
}
}
}
}
代码分析
先分析一下awaitImpl方法,A和D是该方法的关键点,决定着它属于哪一个批次,对应哪一个版本。这里有个小细节,在nexeCycle,cancel解除阻塞时,该线程可能并不在队列中,因为插入队列发生在C处,这在A和D之后(虽然看起来C在D之前,但D取到的queue要在下一次循环时才被当作submitQueue),所以,在E处再进行了一次判断,开始解除阻塞时,旧队列肯定被新队列所替换,newQueue != submitQueue一定为真,就会不调用park进行阻塞了,也就不需要解除阻塞,所以即使解除阻塞时,该线程不在队列中也是没问题的。
再看E处,当进入一个新的cycle时(当前队列与提交的队列不同),a)如果没指定版本,或者到达或超过了指定版本,则返回true;b)如果当前调用了cancel,则当前队列的isCancelQueue将为true,则不继续傻等,返回false;c)或者还未到达指定版本,break,插入到当前队列中,继续等待指定版本的到达。
如果没有进入E处的IF内,则当前线程会被阻塞,直到超时,然后返回false;或被中断,然后抛出InterruptedException;或被解除阻塞,重新进行E处的判定。
这里还有个小细节,既然cancel时,把当前的队列设置了isCancelQueue,那么之后指定版本的await会不会也直接返回了呢?其实不会的,因为它若要执行F处的判断,则先必需通过E处的判定,这意味着,当前队列已经不是提交时的那个设置了isCancelQueue的队列了。
代码中对于cancel的处理,其实并不保证cancel后,之前的await都会被解除阻塞并返回,如果cancel后,紧接着又调用了nextCycle,那么可能某线程感知不到cancel的调用,唤醒后又继续等待指定的版本。cancel的目的是在于不让线程傻等,既然恢复版本更新了,那就继续等待吧。
如果自己维护版本号,则应该保证递增。另外,版本号的设计,考虑到了int溢出的情况,版本的前后判断,我不是使用newVersion>=oldVersion,而是newVersion-oldVersion>=0,这样,版本号就相当于循环使用了,只要两个比较的版本号的差不超过int的最大值,那么都是正确的,int的最大值可是20多亿,几乎不可能出现跨度这么大的两个版本号的比较,所以,认为它是正确的。
小结
本文讲到了一个非阻塞同步算法设计时的小技巧,如果多个变量之间要维护某种特定关系,那么可以将它们封装到一个类中,再用CAS更新这个类的引用,这样就达到了:要么都被更新,要么都没被更新,保持了多个变量之间的一致性。同时需要注意的是,每次更新都必需创建新的包装对象,假如有其它更好的办法,应该避免使用该方法。
非阻塞同步算法实战(二)-BoundlessCyclicBarrier的更多相关文章
- 非阻塞同步算法实战(三)-LatestResultsProvider
本人是本文的作者,首发于ifeve(非阻塞同步算法实战(三)-LatestResultsProvider) 前言 阅读本文前,需要读者对happens-before比较熟悉,了解非阻塞同步的一些基本概 ...
- 非阻塞同步算法与CAS(Compare and Swap)无锁算法
锁(lock)的代价 锁是用来做并发最简单的方式,当然其代价也是最高的.内核态的锁的时候需要操作系统进行一次上下文切换,加锁.释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放. ...
- 【Java并发编程】9、非阻塞同步算法与CAS(Compare and Swap)无锁算法
转自:http://www.cnblogs.com/Mainz/p/3546347.html?utm_source=tuicool&utm_medium=referral 锁(lock)的代价 ...
- Linux非阻塞IO(二)网络编程中非阻塞IO与IO复用模型结合
上文描述了最简易的非阻塞IO,采用的是轮询的方式,这节我们使用IO复用模型. 阻塞IO 过去我们使用IO复用与阻塞IO结合的时候,IO复用模型起到的作用是并发监听多个fd. 以简单的回射服务器 ...
- 七、设备驱动中的阻塞与非阻塞 IO(二)
7.2 轮询 7.2.1 介绍 在用户程序中的 select() 和 poll() 函数最终会使设备驱动中的 poll() 函数被执行. 设备驱动程序中的轮询函数原型: /** 用于询问设备是否可以非 ...
- 使用tornado让你的请求异步非阻塞
http://www.dongwm.com/archives/shi-yong-tornadorang-ni-de-qing-qiu-yi-bu-fei-zu-sai/?utm_source=tuic ...
- tornado异步请求非阻塞
前言也许有同学很迷惑:tornado不是标榜异步非阻塞解决10K问题的嘛?但是我却发现不是torando不好,而是你用错了 比如最近发现一个事情:某网 前言 也许有同学很迷惑:tornado不是标榜异 ...
- [Python]再学 socket 之非阻塞 Server
再学 socket 之非阻塞 Server 本文是基于 python2.7 实现,运行于 Mac 系统下 本篇文章是上一篇初探 socket 的续集, 上一篇文章介绍了:如何建立起一个基本的 sock ...
- tornado异步请求非阻塞-乾颐堂
前言 也许有同学很迷惑:tornado不是标榜异步非阻塞解决10K问题的嘛?但是我却发现不是torando不好,而是你用错了.比如最近发现一个事情:某网站打开页面很慢,服务器cpu/内存都正常.网络状 ...
随机推荐
- oracle_exp_query_where_clause
每次都忘记这个斜线,记录一下 exp img/123@orclxxx file=d:\bbb.dmp table=(ccc_tab) query=\"where id > 999\&q ...
- ubuntu 'Unable to correct problems, you have held broken packages' 错误
在用apt 安装软件时,有时会用国内的源以加快下载速度. 但是在使用ubuntu 14.04的过程中,这一过程可能会导致错误“Unable to correct problems, you have ...
- High CPU Usage 原因及分析
常见的高CPU利用率出现几个原因: Missing Index 统计信息过时 非SARG查询 Implicit Conversions Parameter Sniffing Non-parameter ...
- FFT的分析以及matlab实验
FFT(Fast Fourier Transformation),即为快速傅氏变换,是离散傅氏变换(DFT)的快速算法. 采样得到的数字信号,做FFT变换,N个采样点,经过FFT之后,就可以得到N个点 ...
- ASP.NET 表单验证实现浅析
首先,自然是配置 Web.config,在 <system.web> 下设定: <authentication mode="Forms"> <form ...
- 我在使用vs进行C#编程中常用的几个快捷键
在vs中进行C#编程时: (1)输入svm然后按Tab键会生成Main函数: (2)Ctrl +k+s 三个键一起按,会调出代码段:选中多行后,然后按以上三个快捷键,输入需要使用的代码段,按下Tab, ...
- information_schema系列八(事物,锁)
今天我们主要看一下MySQL information_schema里面的关于innodb的锁和事物的两三个系统表: 看一下锁对应的sql: select * from innodb_lock_wait ...
- [原创]多版本Java环境变量的配置
起因: 偶然突发兴致, 收拾下自己的老T500电脑, 用来做个家庭开发用机. 应为每次装系统都有GHOST备份的习惯, 所以需要提前搭建好开发环境. 而且新装系统的目的之一, 也是想研究下Andr ...
- myeclicps开发web时候复制一个工程改名字后执行出现404错误
当部署的时候会出现如下的警告,此时请按照如下的方法解决: 1.选择要运行的工程,右击-->properties-->在上边的搜索框中搜索web,选中web,将Web Context-roo ...
- 机器学习中的范数规则化之(一)L0、L1与L2范数
L1正则会产生稀疏解,让很多无用的特征的系数变为0,只留下一些有用的特征 L2正则不让某些特征的系数变为0,即不产生稀疏解,只让他们接近于0.即L2正则倾向于让权重w变小.见第二篇的推导. 所以,样本 ...