为什么CLH Lock需要在java中使用prev-Node

时间:2017-04-26 07:58:02

标签: java multithreading concurrency synchronized spinlock

这是java中典型的CLH-Lock:

public class CLHLock{  

    private final AtomicReference tail;

    // why we need this node?  
    private final ThreadLocal myPred;  

    private final ThreadLocal myNode;  

    public CLHLock() {  
        tail = new AtomicReference(new QNode());  
        myNode = new ThreadLocal() {  
            protected QNode initialValue() {  
                return new QNode();  
            }  
        };  

        myPred = new ThreadLocal();  
    }  

    public void lock() {  
        QNode node = myNode.get();  
        node.locked = true;  
        QNode pred = tail.getAndSet(node); 

        // this.myPred == pred 
        myPred.set(pred);  
        while (pred.locked) {  
        }  
    }  

    public void unlock() {  
        QNode node = myNode.get();  
        node.locked = false;  

        // this.myNode == this.myPred
        myNode.set(myPred.get());  
    }  

    private static class QNode {  
        volatile boolean locked;  
    }  
}

为什么我们需要myPred Node,只有两个地方使用了这个变量:

  1. this.prev.set(pred);
  2. 列出项目this.node.set(this.prev.get());
  3. 当我们完成时,this.prev == this.node == pred

    也许我们可以这样实现:

    public class CLHLock {
        // Node tail
        private final AtomicReference<QNode> tail = new AtomicReference<>(new QNode());
    
        // ThreadLocal
        private final ThreadLocal<QNode> node = ThreadLocal.withInitial(QNode::new);
    
        public void lock() {
            QNode now = node.get();
            now.locked = true;
    
            // spin on pre-node
            QNode pre = tail.getAndSet(now);
            while (pre.locked) {
            }
        }
    
        public void unlock() {
            QNode now = node.get();
            now.locked = false;
        }
    
        class QNode {
            volatile boolean locked = false;
        }
    }
    

    以上两者有什么区别?

2 个答案:

答案 0 :(得分:3)

第二个实现容易出现死锁。

假设你有两个线程,T1和T2。 T1拥有锁,T2等待T1释放。

T1.node.locked为真,T2.node.locked为真,尾部指向T2.node,T2正在pre.locked上旋转,这是T1的节点。

现在T1释放锁定(将T1.node.locked设置为false),然后在T2尝试抢占时再次尝试获取锁定。 T1.node.locked再次成为现实,但尾部为T2.node,因此T1现在正在等待T2。并且T2仍在等待T1的同一节点,现在已被锁定!死锁。

第一个实现通过重用不是当前的,但不是前一个(前任)节点来保护你免受它的影响,所以这种情况是不可能的:前任是null(然后没有什么可以重用)或者没有,那么它的节点当它解锁时重复使用。

答案 1 :(得分:0)

第二种实现无效。 假设线程一个首先拥有该锁,然后释放该锁。尾巴现在是线程一个的节点。但是线程一再次获取该锁,就会发现线程一将永远旋转。无论如何只要稍微改变一下解锁方法,则不需要pred节点。

public void unlock() {  
    QNode node = myNode.get();  
    node.locked = false;  

    // this.myNode == this.myPred
    myNode.set(new Node());  
}

它也会工作。