收藏 分享(赏)

非阻塞同步算法-LatestResultsProvider实现.doc

上传人:j35w19 文档编号:9220338 上传时间:2019-07-30 格式:DOC 页数:8 大小:95KB
下载 相关 举报
非阻塞同步算法-LatestResultsProvider实现.doc_第1页
第1页 / 共8页
非阻塞同步算法-LatestResultsProvider实现.doc_第2页
第2页 / 共8页
非阻塞同步算法-LatestResultsProvider实现.doc_第3页
第3页 / 共8页
非阻塞同步算法-LatestResultsProvider实现.doc_第4页
第4页 / 共8页
非阻塞同步算法-LatestResultsProvider实现.doc_第5页
第5页 / 共8页
点击查看更多>>
资源描述

1、非阻塞同步算法LatestResultsProvider 实现前言本文主要为 happens-before 法则的灵活运用,解决问题的小技巧和分析问题的方式。背景介绍原始需求为:本人当时在编写一个正则替换工具,里面会动态地显示所有的匹配结果(包括替换预览) ,文本、正则表达式、参数,这些数据的其中一项发生 了变化,结果就应该被更新,为了提供友好的交互体验,数据变化时,应该是发起一个异步请求,由另一个独立的线程来完成运算,完成后通知 UI 更新结果。由于 是动态显示,所以提交会非常频繁。需求描述需要这样一个工具类,允许用户频繁地提交数据(本文之后以“submit”表示该操作)和更新结果(本文之后

2、以“update” 表示该操 作) ,submit 时,如果当前有进行中的运算,则应该取消,使用新参数执行新的运算;update 时,如果当前没有进行中的运算(处于阻塞状态) ,并且 当前结果不是最新的,则唤醒该线程,使用当前的新数据,执行新的运算。此处之所以分为 submit 和 update 两个方法,是为了支持手动更新,即点击更 新按钮时,才更新结果。此外,出于练手的原因,也出于编写一个功能全面,更实用的工具的目的,我还加入了一些额外的需求:1、引入多线程场景,update 和 submit 均可由多个线程同时发起,该工具类应设计成线程安全的。2、允许延迟执行运算,如果延时内执行 sub

3、mit,仅重新计算延时。如果运算不方便取消,在短时间频繁 submit 的场景下,延时会是一个很好的应对办法。3、允许设置一个最大延迟时间,作为延迟开启运算的补充。当长时间频繁 submit 时,会形成这样的局面,一直未进入运算环节,新结果计算不出来,上一次计算结果却是很早以前的。如果需要显示一个较新但不是最新的结果,最大延迟时间将会很有用。4、提供主动取消方法,主动取消正在进行的运算。5、update 时,允许等待运算完成,同时也可设置超时时间。当主动取消、超时、完成了当前或更(更加的意思)新的数据对应的运算时,结束等待。需求交待完了,有兴趣有精力的读者,可以先试着思考下怎么实现。问题分析该

4、工具应该维护一个状态字段,这样才能在发起某个操作时,根据所处的状态作出正确的动作,如:如果当前不处于停止状态(或者主动取消状态,原因见下文) ,执行 update就不需要唤醒运算线程。简单分析可知,至少应该有这样几种状态:1、停止状态:当前没有运算任务,线程进入阻塞状态,主动取消和运算完成后,进入该状态2、延迟状态:设置了延迟开启运算时,进入运算前,处于该状态3、运算状态:正在执行运算4、主动取消状态:当发起主动取消时,进入该状态5、新任务状态:当时有新的运算任务时,进入该状态,然后重新进入运算状态延迟再来看一下延迟,如果延迟 500 毫秒,就每次 sleep(500),那么期间再 submi

5、t 怎么办?将它唤醒然后重新 sleep(500)吗?显然不行,成本太大了。我有一个小技巧:将 500 分成多个合适的等份,使用一个计数器,每次 sleep 一个等份,计数器加 1,如果发起 submit,仅把计数器置 0 即可,虽然看起来线程的状态切换变多了,但应对频繁重置时,它更稳定。虽然时间上会上下波动一个等份,但此处并不需要多么精确。现在还面临这样一个问题,如何知道当前是处于延迟状态并计数器置 0?取出状态值进行判断,然后置 0,这方法显然不行,因为置 0 的时候,可能状态已经变了,所以你无法知道该操作是否生效了。我想到的办法是,再引入一个延迟重置状态。如果处于该状态,则下一次计数器加

6、 1时,将计数器重置,状态变更是可以知道成功与否的。状态变更有些状态的变更是有条件的,比如说当前处于取消状态,就不能把它转为运算状态,运算状态只能由新任务状态、延迟状态(延迟完成后执行运算)或延迟重置状态转入。这种场景正好跟 CAS 一致,所以,使用一个 AtomicInteger 来表示状态。分析下各状态之间的转换,可以得出下面的状态变更图:蓝色的 a(bcd)|(e)f 线路为停止状态下,发起一次 update,运算完重新回到停止的过程,开启延迟时是 bcd,否则是 e。红色的线 j 表示超过了最大延迟时间,退出延迟,进入运算状态(也可以是 d) 。绿色的线 ghi(包括 a)表示:如果发

7、起了 submit 或 update,状态应该怎么改变。如果处于延迟重置、新任务则不需要进行任何操作;如果处 于延迟状态,则转为延迟重置即可;如果处于运算状态,则可能使用了旧参数,应该转为新任务;如果为主动取消或停止状态,并且是调用 update 方法,则转 为新任务,并且可能处于阻塞状态,应该唤醒该线程。黑色的线 l 表示,可在任意状态下发起主动取消,进入该状态。然后通知等待线程后,转入停止状态,对应紫色的 k,如果在停止状态下发起主动取消,则仅转为主动取消状态,不会通知等待线程。所以当线程阻塞时,可能处于停止状态或者主动取消状态。顺序问题上面已经分析到,当 submit 时,应该把延迟转为

8、延迟重置、或运算转为新任务,这两个尝试的顺序是不是也有讲究呢?是的,因为正常执行流程 a(bcd)|(e)f 中,运算状态在延迟状态之后,假如先尝试运算转为新任务,可能此时为延迟状态,故失败,再尝试延迟转 为延迟重置时,状态在这期间从刚才的延迟转为了运算,故两次尝试都失败了,本应该重置延迟的,却什么也没干,这是错误的。而将两次尝试顺序调换一下,只要 状态为延迟或运算,那么两次状态转换尝试中,一定有一次会成功。之后的代码中还有多处类似的顺序细节。解决方案下面给出完整的代码,除去等待运算完成那部分,其它地方均为 wait-free 级别的实现。calculateResult 是具体执行运算的方法;

9、 1 /*2 * author 3 * date 2013-2-24 */5 public abstract class LatestResultsProvider 6 /* update return value */7 public static final int UPDATE_FAILED = -1;8 public static final int UPDATE_NO_NEED_TO_UPDATE = 0;9 public static final int UPDATE_SUCCESS = 1;10 public static final int UPDATE_COMMITTED =

10、 2;11 /* update return value */12 13 /* work states*/14 private static final int WS_OFF = 0;15 private static final int WS_NEW_TASK = 1;16 private static final int WS_WORKING = 2;17 private static final int WS_DELAYING = 3;18 private static final int WS_DELAY_RESET = 4;19 private static final int WS

11、_CANCELED = 5;20 /* work states*/21 private final AtomicInteger workState;22 23 private int sleepPeriod = 30;24 25 private final AtomicInteger parametersVersion;26 private volatile int updateDelay;/ updateDelay=027 private volatile int delayUpperLimit;28 29 private final BoundlessCyclicBarrier barri

12、er;30 private Thread workThread;31 32 /*33 *34 * param updateDelay unit: millisecond35 * param delayUpperLimit limit the sum of the delay, disabled36 * while delayUpperLimit 0 ? WS_DELAY_RESET : WS_WORKING) 59 if (workSpareAndSet(WS_CANCELED, WS_OFF) 60 barrier.cancel();61 62 LockSupport.park();63 i

13、nterrupted();64 65 if (workState.get() = WS_DELAY_RESET) 66 int delaySum = 0;67 for (;) 68 if (workSpareAndSet(WS_DELAY_RESET,69 WS_DELAYING) 70 sleepCount = (updateDelay + sleepPeriod - 1)71 / sleepPeriod;72 73 sleep(sleepPeriod);74 if (-sleepCount = 0) 79 delaySum += sleepPeriod;80 if (delaySum =

14、delayUpperLimit) 81 if (!workSpareAndSet(82 WS_DELAYING, WS_WORKING)83 workSpareAndSet(84 WS_DELAY_RESET, WS_WORKING);85 break;86 87 88 if (workState.get() != WS_DELAYING89 91 92 93 if (isWorking() 94 int workingVersion = parametersVersion.get();95 try 96 calculateResult();97 if (workSpareAndSet(WS_

15、WORKING, WS_OFF)98 barrier.nextCycle(workingVersion);99 catch (Throwable t) 100 t.printStackTrace();101 workState.set(WS_CANCELED);102 103 104 catch (InterruptedException e) 105 workSpareAndSet(WS_DELAYING, WS_CANCELED);106 workSpareAndSet(WS_DELAY_RESET, WS_CANCELED);107 108 / for(;)109 / run()110

16、;111 workThread.setDaemon(true);112 workThread.start();113 114 115 public int getUpdateDelay() 116 return updateDelay;117 118 119 /*120 * param updateDelay121 * delay time. unit: millisecond122 */123 public void setUpdateDelay(int updateDelay) 124 this.updateDelay = updateDelay = 0 ? UPDATE_SUCCESS1

17、68 : UPDATE_FAILED;169 170 171 /*172 * return FAILED, NO_NEED_TO_UPDATE, SUCCESS173 * throws InterruptedException174 */175 public final int updateAndWait() throws InterruptedException 176 return updateAndWait(0);177 178 179 public final boolean isResultUptodate() 180 return parametersVersion.get() =

18、 barrier.getVersion();181 182 183 /*184 * be used in calculateResult()185 * return true: the work state is working, worth to calculate the186 * result absolutely, otherwise you can cancel the current calculation187 */188 protected final boolean isWorking() 189 return workState.get()=WS_WORKING;190 1

19、91 192 /*193 * you must call this after update the parameters, and before calling the194 * update195 */196 protected final void updateParametersVersion() 197 int pVersion = parametersVersion.get();198 /CAS failed means that another thread do the same work already199 if (parametersVpareAndSet(pVersio

20、n, pVersion + 1)200 if (!workSpareAndSet(WS_DELAYING, WS_DELAY_RESET)201 workSpareAndSet(WS_WORKING, WS_NEW_TASK);202 203 204 /*205 * implement this to deal with you task206 */207 protected abstract void calculateResult();208 代码中,我直接在构造方法里开启了新的线程,一般来说,是不推荐这样做的,但在此处,除非在构造还未完成时就执行 update 方法,否则不会引发什么问题

21、。小结状态变更非常适合使用非阻塞算法,并且还能够达到 wait-free 级别。非阻塞同步相对于锁同步而言,由代码块,转为了点,是另一种思考方式。有时,无法做到一步完成,也许可以分成两步完成,同样可以解决问题,ConcurrentLinkedQueue 就是这么做的。如果需要维护多个数据之间的某种一致关系,则可以将它们封装到一个类中,更新时采用更新该类对象的引用的方式。众所周知,锁同步算法是难以测试的,非阻塞同步算法更加难以测试,我个人认为,其正确性主要靠慎密的推敲和论证。非阻塞同步算法比锁同步算法要显得更复杂些,如果对性能要求不高,对非阻塞算法掌握得还不太熟练,建议不要使用非阻塞算法,锁同步算法要简洁得多,也更容易维护,如上面所说的,两条看似没有顺序的语句,调换下顺序,可能就会引发 BUG。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报