摘要:如果停止了版本更新,可使用方法来解除所有因而阻塞的线程,包括指定版本号的。如果自己维护版本号,则应该保证递增。
前言
相比上一篇而言,本文不需要太多的准备知识,但技巧性更强一些。因为分析、设计的过程比较复杂繁琐,也限于篇幅,所以,主要展示如何解决这些需求,和讲解代码。另外,所讲的内容也是后一篇实战中需要用到的一个工具类。
需求介绍我需要编写一个同步工具,它需要提供这样几个方法: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代码分析waitQueueRef; public BoundlessCyclicBarrier() { this(0); } public BoundlessCyclicBarrier(int startVersion) { waitQueueRef = new AtomicReference (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更新这个类的引用,这样就达到了:要么都被更新,要么都没被更新,保持了多个变量之间的一致性。同时需要注意的是,每次更新都必需创建新的包装对象,假如有其它更好的办法,应该避免使用该方法。
via ifeve.com
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/64034.html
摘要:黑色的线表示,可在任意状态下发起主动取消,进入该状态。所以当线程阻塞时,可能处于停止状态或者主动取消状态。非阻塞同步相对于锁同步而言,由代码块,转为了点,是另一种思考方式。 前言 阅读本文前,需要读者对happens-before比较熟悉,了解非阻塞同步的一些基本概念。本文主要为happens-before法则的灵活运用,和一些解决问题的小技巧,分析问题的方式。 背景介绍 原始需...
摘要:前言学习情况记录时间子目标多线程记录在学习线程安全知识点中,关于的有关知识点。对于资源竞争严重线程冲突严重的情况,自旋的概率会比较大,从而浪费更多的资源,效率低于。 前言 学习情况记录 时间:week 1 SMART子目标 :Java 多线程 记录在学习线程安全知识点中,关于CAS的有关知识点。 线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表...
摘要:注意这里指的不是当次而是之后,所以如果我们使用队列的方法返回,就知道队列是否为空,但是不知道之后是否为空,并且,当关注的操作发生时,在插入或取出操作的返回值里告知此信息,来指导是否继续注册写操作。 前言 本文写给对ConcurrentLinkedQueue的实现和非阻塞同步算法的实现原理有一定了解,但缺少实践经验的朋友,文中包括了实战中的尝试、所走的弯路,经验和教训。 背景介绍 ...
摘要:如问到是否使用某框架,实际是是问该框架的使用场景,有什么特点,和同类可框架对比一系列的问题。这两个方向的区分点在于工作方向的侧重点不同。 [TOC] 这是一份来自哔哩哔哩的Java面试Java面试 32个核心必考点完全解析(完) 课程预习 1.1 课程内容分为三个模块 基础模块: 技术岗位与面试 计算机基础 JVM原理 多线程 设计模式 数据结构与算法 应用模块: 常用工具集 ...
摘要:相比与其他操作系统包括其他类系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。因为多线程竞争锁时会引起上下文切换。减少线程的使用。很多编程语言中都有协程。所以如何避免死锁的产生,在我们使用并发编程时至关重要。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)syn...
阅读 1914·2023-04-26 01:56
阅读 3110·2021-11-18 10:02
阅读 3049·2021-09-09 11:35
阅读 1283·2021-09-03 10:28
阅读 3407·2019-08-29 18:36
阅读 2846·2019-08-29 17:14
阅读 832·2019-08-29 16:10
阅读 1615·2019-08-26 13:45