zoukankan      html  css  js  c++  java
  • 实现TOLock过程中的一处多线程bug

    背景

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

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

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

    public class TOLock implements Lock {
    
        private static final QNode AVAILABLE = new QNode();
    
        private AtomicReference<QNode> tail;
    
        private ThreadLocal<QNode> myNode;
    
        public TOLock() {
            this.tail = new AtomicReference<>(null);
            this.myNode = new ThreadLocal<>();
        }
    
        @Override
        public void lock() {
            QNode qNode = new QNode();
            qNode.pred = null;
            myNode.set(qNode);
    
            QNode myPred = tail.getAndSet(qNode);
            if (myPred == null) {
                return;
            }
    
            while (myPred.pred != AVAILABLE) {
                if (myPred.pred != null) {
                    myPred = myPred.pred;
                }
            }
        }
    
        @Override
        public void unlock() {
            QNode qNode = myNode.get();
            if (!tail.compareAndSet(qNode, null)) {
                qNode.pred = AVAILABLE;
            }
        }
    
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            long startTime = System.currentTimeMillis();
            long patience = TimeUnit.MILLISECONDS.convert(time, unit);
    
            QNode qNode = new QNode();
            myNode.set(qNode);
            qNode.pred = null;
    
            QNode myPred = tail.getAndSet(qNode);
            if (myPred == null || myPred.pred == AVAILABLE) {
                return true;
            }
    
            while (System.currentTimeMillis() - startTime < patience) {
                QNode predPred = myPred.pred;
                if (predPred == AVAILABLE) {
                    return true;
                } else if (predPred != null) {
                    myPred = predPred;
                }
            }
    
            if (!tail.compareAndSet(qNode, myPred)) {
                qNode.pred = myPred;
            }
            return false;
        }
    
        private static class QNode {
            volatile QNode pred;
        }
    }
    

    问题

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

    while (myPred.pred != AVAILABLE) {
        if (myPred.pred != null) {
            myPred = myPred.pred;
        }
    }
    

    这段代码中的myPred.pred为null,所以就一直走不出去。myPred.pred为什么会是null,再定睛一看,myPred居然就是AVAILABLE。这怎么会呢?
    myPred的取值路径只有两种,一种是从tail通过GAS操作拿到tail之前的值,另一种就是走循环拿到myPred.pred。
    原子引用tail一开始为null,每次都是吸一个new出来的QNode进队列,绝对不可能会有AVAILABLE的情况。

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

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

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

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

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

    QNode predPred;
    while ((predPred = myPred.pred) != AVAILABLE) {
        if (predPred != null) {
            myPred = predPred;
        }
    }
    

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

    后记

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

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

     private InputStream getInIfOpen() throws IOException {
        InputStream input = in;
        if (input == null)
            throw new IOException("Stream closed");
        return input;
    }
    

    JDK中getInIfOpen是这样写的,虽然看上去挺丑的,像是冗余了一个input变量,但还真就是改不得。
    否则就会出现在读取input的时候还不是null,return出去就已经是null的情况了。

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


    完整源码实现

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

  • 相关阅读:
    关于cmake、make、make install
    windows开启ip_forwarding功能
    最新devstack安装(ussuri)
    【rabbitmq】之业务封装
    【rabbitmq】之过期和死信队列
    【rabbitmq】之confirm和return机制
    【rabbitmq】之消费端手动ack
    java短网址服务
    详解druid打印SQL日志
    logback配置文件拆分,抽取公共配置
  • 原文地址:https://www.cnblogs.com/micrari/p/6747184.html
Copyright © 2011-2022 走看看