背景

最近在啃《多处理器编程的艺术》,书中的7.6节介绍了时限锁——实现了tryLock方法的队列锁。

书中重点讲解了tryLock的实现,也就是如何实现在等待超时后退出队列,放弃锁请求,并且能让后继线程感知到。

在实现的过程中,我为TOLock补充了lock方法的实现。代码如下所示:

  1. public class TOLock implements Lock {
  2. private static final QNode AVAILABLE = new QNode();
  3. private AtomicReference<QNode> tail;
  4. private ThreadLocal<QNode> myNode;
  5. public TOLock() {
  6. this.tail = new AtomicReference<>(null);
  7. this.myNode = new ThreadLocal<>();
  8. }
  9. @Override
  10. public void lock() {
  11. QNode qNode = new QNode();
  12. qNode.pred = null;
  13. myNode.set(qNode);
  14. QNode myPred = tail.getAndSet(qNode);
  15. if (myPred == null) {
  16. return;
  17. }
  18. while (myPred.pred != AVAILABLE) {
  19. if (myPred.pred != null) {
  20. myPred = myPred.pred;
  21. }
  22. }
  23. }
  24. @Override
  25. public void unlock() {
  26. QNode qNode = myNode.get();
  27. if (!tail.compareAndSet(qNode, null)) {
  28. qNode.pred = AVAILABLE;
  29. }
  30. }
  31. @Override
  32. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  33. long startTime = System.currentTimeMillis();
  34. long patience = TimeUnit.MILLISECONDS.convert(time, unit);
  35. QNode qNode = new QNode();
  36. myNode.set(qNode);
  37. qNode.pred = null;
  38. QNode myPred = tail.getAndSet(qNode);
  39. if (myPred == null || myPred.pred == AVAILABLE) {
  40. return true;
  41. }
  42. while (System.currentTimeMillis() - startTime < patience) {
  43. QNode predPred = myPred.pred;
  44. if (predPred == AVAILABLE) {
  45. return true;
  46. } else if (predPred != null) {
  47. myPred = predPred;
  48. }
  49. }
  50. if (!tail.compareAndSet(qNode, myPred)) {
  51. qNode.pred = myPred;
  52. }
  53. return false;
  54. }
  55. private static class QNode {
  56. volatile QNode pred;
  57. }
  58. }

问题

在编写lock方法的单元测试的时候发现,TOLock偶现卡死,当加大线程池中线程数量,几乎是稳定复现卡死。遂debug,发现lock方法卡死是因为

  1. while (myPred.pred != AVAILABLE) {
  2. if (myPred.pred != null) {
  3. myPred = myPred.pred;
  4. }
  5. }

这段代码中的myPred.pred为null,所以就一直走不出去。myPred.pred为什么会是null,再定睛一看,myPred居然就是AVAILABLE。这怎么会呢?

myPred的取值路径只有两种,一种是从tail通过GAS操作拿到tail之前的值,另一种就是走循环拿到myPred.pred。

原子引用tail一开始为null,每次都是吸一个new出来的QNode进队列,绝对不可能会有AVAILABLE的情况。

排除了所有不可能的情况,剩下的即使在不可能,也都是真相了。

事实上就是如此,myPred变为AVAILABLE是通过循环的if分支拿到的。

  1. if (myPred.pred != null) {
  2. myPred = myPred.pred;
  3. }

这段while循环在多线程的情况下是有bug的,在并发环境下,在后继争用线程读取while条件的时候,有可能前驱线程还没有释放锁,所以此时的myPred.pred应该为null,接下去前驱线程释放了锁,此时myPred.pred为AVAILABLE,这时当前线程理应具备进入临界区的条件,但是因为内存循环的判断导致myPred被赋值为AVAILABLE,此后myPred.pred永远为null,线程无限时原地旋转,后继线程也无法进入临界区。

我随后将此处代码改为如下:

  1. QNode predPred;
  2. while ((predPred = myPred.pred) != AVAILABLE) {
  3. if (predPred != null) {
  4. myPred = predPred;
  5. }
  6. }

其实就和tryLock中的写法是类似的,将myPred.pred赋值到局部变量,封闭在栈上即可。

后记

后来仔细想了想,其实类似的写法在我以前读源码的时候我有注意到过,例如BufferedInputStream里面也有很多类似的将变量封闭在栈上保证线程安全的做法。可以参考这篇帖子

在FilterInputStream里面定义的in是

protected volatile InputStream in可以被子类访问到,或者BufferedInputStream本身的byte[] buf也是protected volatile的并且还被volatile修饰,本身就是为多线程环境设计的,所以我们可以看到

  1. private InputStream getInIfOpen() throws IOException {
  2. InputStream input = in;
  3. if (input == null)
  4. throw new IOException("Stream closed");
  5. return input;
  6. }

JDK中getInIfOpen是这样写的,虽然看上去挺丑的,像是冗余了一个input变量,但还真就是改不得。

否则就会出现在读取input的时候还不是null,return出去就已经是null的情况了。

看过类似代码,积累过知识,自己真正操刀练习的时候还是犯错,只说明了一句话:纸上得来终觉浅,绝知此事要躬行。


完整源码实现

关于TOLock的完整代码实现,可以参考我的github上的实现

实现TOLock过程中的一处多线程bug的更多相关文章

  1. Coding过程中遇到的一些bug

    1. 在使用layoutSubviews方法调整自定义view内部的子控件坐标时,最好不要使用子控件的centerX,centerY属性,否则会出现奇怪的bug. 如果一定要用,务必仔细检查,该子控件 ...

  2. vue 使用过程中自己遇到的bug

    需要安装npm git(windows系统需要安装) npm 是node的包管理工具 npm 国内的网站比较慢,推荐使用cnpm(淘宝的镜像) cnpm(npm) install 创建依赖-----因 ...

  3. 写js过程中遇到的一个bug

    <div class="func_Div" id="xxcx"><span>信息查询</span>              ...

  4. Bug,项目过程中的重要数据

    作者|孙敏 为什么要做Bug分析? Bug是项目过程中的一个有价值的虫子,它不只是给开发的,而是开给整个项目组的. 通过Bug我们能获得什么? 积累测试方法,增强QA的测试能力,提升产品质量 发现项目 ...

  5. 【转】C 编译器优化过程中的 Bug

    C 编译器优化过程中的 Bug 一个朋友向我指出一个最近他们发现的 GCC 编译器优化过程(加上 -O3 选项)里的 bug,导致他们的产品出现非常诡异的行为.这使我想起以前见过的一个 GCC bug ...

  6. Junit使用过程中需要注意的诡异bug以及处理办法

    在开发过程中我们有时会遇到狠多的问题和bug,对于在编译和运行过程中出现的问题很好解决,因为可以在错误日志中得到一定的错误提示信息,从而可以找到一些对应的解决办法.但是有时也会遇到一些比较诡异的问题和 ...

  7. ssd运行过程中遇到的bug

    1.出现以下错误: 没有添加环境变量: https://github.com/weiliu89/caffe/issues/4 可以看到当前PYTHONPATH不再ssd1里面,所以需要修改,修改之后就 ...

  8. ltib安装过程中遇到好多问题,从网上转来的好多份总结

    最近调试MPC5125的板子,第一步LTIB都装不过去,挫败感十足. LTIB的安装镜像来自于freescale的ltib-mpc5121ads-200906,是用于Ubuntu 10版本之前的,现在 ...

  9. ASP.NET MVC Filters 4种默认过滤器的使用【附示例】 数据库常见死锁原因及处理 .NET源码中的链表 多线程下C#如何保证线程安全? .net实现支付宝在线支付 彻头彻尾理解单例模式与多线程 App.Config详解及读写操作 判断客户端是iOS还是Android,判断是不是在微信浏览器打开

    ASP.NET MVC Filters 4种默认过滤器的使用[附示例]   过滤器(Filters)的出现使得我们可以在ASP.NET MVC程序里更好的控制浏览器请求过来的URL,不是每个请求都会响 ...

随机推荐

  1. 在 JavaScript 中 prototype 和 __proto__ 有什么区别

    本文主要讲三个 问题 prototype 和 proto function 和 object new 到底发生了什么 prototype 和 proto 首先我们说下在 JS 中,常常让我们感到困惑的 ...

  2. java基础:数组的拼接

  3. 深入理解java虚拟机之——JVM垃圾回收策略总结

    如何判断一个对象是否存活 引用计数算法:给对象中添加一个引用计数器,每当有引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就是不可能再被使用.  Java虚拟机里面没有 ...

  4. Maven项目搭建(一):Maven初体验

    今天给大家介绍一个项目管理和综合工具:Maven. Maven: maven读作 ['meivin],本意是指可以被信任的领域专家,致力于传播知识(来自于http://en.wikipedia.org ...

  5. iOS开发RunLoop

    最近处于离职状态,时间也多了起来,但是学习还是不能放松,今天总结一下RunLoop,RunLoop属于iOS系统层的东西,还是比较重要的. 一.什么是RunLoop 字面意思看是跑圈,也可以看作运行循 ...

  6. delphi处理消息的几种方式

    第一种:自定义处理单条消息 unit Unit2; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, C ...

  7. UI自动化测试表单重要代码

    public class frame { public static void main(String[] args) { // TODO Auto-generated method stub Sys ...

  8. spring-mvc-两个个小例子

    1.用Eclipse创建一个工程,命名为spring2.0 并添加相应的jar包(我用的是4.0.5的版本)到 lib 包下: spring-webmvc-4.0.5.RELEASE.jar spri ...

  9. Struts2的validator和WEB-INF下页面交互以及路径问题

    当我使用短路校验器时(客户端),在页面下方老是出来 FreeMarker template error!然后我就把我的页面都放在了WEB-INF中,结果很多路径都不对了,因为客户端是没有直接访问Str ...

  10. pyhton中的Queue(队列)

    什么是队列? 队列就像是水管子,先进先出,与之相对应的是栈,后进先出. 队列是线程安全的,队列自身有机制可以实现:在同一时刻只有一个线程在对队列进行操作. 存数据,取数据 import Queue q ...