CLH锁 简介

转自CLH锁 简介 - gaob2001的个人空间 - OSCHINA - 中文开源技术交流社区

概述

在学习Java AQS框架的时候发现加锁的逻辑非常奇怪,后来得知加锁逻辑是CLH锁的一个变种,于是了解一下,对于理解AQS框架有好处。

简介

CLH锁是有由Craig, Landin, and Hagersten这三个人发明的锁,取了三个人名字的首字母,所以叫 CLH Lock。

CLH锁主要有一个QNode类,QNode类内部维护了一个boolean类型的变量,每个线程拥有一个前驱节点(myPred)和当前自己的节点(myNode),还有一个tail节点用于存储最后一个获取锁的线程的状态。CLH的从逻辑上形成一个锁等待队列从而实现加锁,CLH锁只支持按顺序加锁和解锁,不支持重入,不支持中断。

Java实现

public class CLHLock {
    private final AtomicReference<QNode> tail;
    private final ThreadLocal<QNode> myPred;
    private final ThreadLocal<QNode> myNode;

    private static class QNode {
        volatile boolean locked = false;
    }

    public CLHLock() {
        tail = new AtomicReference<QNode>(new QNode());
        myNode = new ThreadLocal<QNode>() {
            @Override
            protected QNode initialValue() {
                return new QNode();
            }
        };
        myPred = new ThreadLocal<QNode>() {
            @Override
            protected QNode initialValue() {
                return null;
            }
        };
    }

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

    public void unlock() {
        QNode qnode = myNode.get();
        qnode.locked = false;
        myNode.set(myPred.get());
        // myNode.set(new QNode());
    }
}

代码很简单,tail变量的类型是AtomicReference用于保证原子操作,myNode是ThreadLocal类型的线程本地变量,保存当前节点的状态,myPred是ThreadLocal类型的线程本地变量,保存等待节点的状态。

测试

先通过简单的测试看一下效果

public static void main(String[] args) {
    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
        }
    };

    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
}

声明了一个runnable对象,在线程内执行从1累加到10000,最后打印一个结果。在多线程的环境下这个a++不是一个原子操作,所以最后的计算结果一定是不正确的。

Thread-0 a = 11758
Thread-1 a = 15091
Thread-2 a = 18309
Thread-3 a = 18831
Thread-4 a = 23398
Thread-5 a = 23686
Thread-6 a = 33686

运行一次之后是这样的结果,和预期一样。然后加上锁看一下

public static void main(String[] args) {
    CLHLock lock = new CLHLock();

    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            lock.lock();
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
            lock.unlock();
        }
    };

    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
    new Thread(runnable).start();
}

创建了一个CLHLock对象,调用了 lock.lock() 和 lock.unlock()。把整个run方法里面的内容都锁住,也就是等一个线程运行完了这个累加,下一个线程才可以继续执行,否则只能等着。

Thread-0 a = 10000
Thread-1 a = 20000
Thread-2 a = 30000
Thread-3 a = 40000
Thread-4 a = 50000
Thread-5 a = 60000
Thread-6 a = 70000

现在多次运行之后都是这个结果,加锁有效果。

原理分析

我们仔细分析一下lock和unlock的代码

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

public void unlock() {
    QNode qnode = myNode.get();
    qnode.locked = false;
    myNode.set(myPred.get());
}

锁的代码很简单就这么几行

结合这个图从上往下看,场景是有2个线程(Thread1, Thread2)同时想获取锁执行任务,左边是Thread1的执行情况,右边是Thread2的执行情况。这里的myNode myPred都是threadlocal类型的,下面说的myNode myPred的状态都是指myNode myPred内的QNode的状态。

第一行是初始化之后的状态,各个QNode都是false。

第二行第三行开始执行lock的操作,先将myNode的状态改为true,再将myNode的引用赋值给tail(tail.getAndSet(node) 的意思是将tail设置为node并返回tail原来的值,这里tail存的是一个QNode对象),再把tail原来的值赋值给myPred,通过一个while循环判断myPred的状态是否为true,为true表示锁正在被占用需要等待,一旦myPred变为false表示锁被释放了,可以执行。那么结合2个线程的情况来看,thread1调用lock方法成功获取到锁,thread2同时也调用lock方法想要获取锁,执行到 tail.getAndSet(node)的时候将tail设置为thread2.myNode,然后获取tail的旧值设置到thread2.myPred,那这个时候tail的旧值是刚才thread1的myNode,也就是说thread2在执行 while(pred.locked){} 等待的时候其实等待的是thread1.myNode状态变为false。tail存储的只是最后一个获取锁的线程的QNode,myNode一直在myPred上等待,通过一个while循环来实现独占锁。

第四行开始执行unlock操作,thread1任务执行完了将myNode的状态设置为false,此时thread2.myPred因为持有的是thread1.myNode的引用,所以也变为false退出循环,thread2得以执行下面的任务。

第五行,将myNode的值设置为myPred的引用。

看上去第五行似乎没有什么必要,网上关于这个的说话比较多,说一下我的理解。如果没有这行代码,在上面这个图中thread2线程在等待thread1.myNode的状态,假设thread1任务执行的速度非常快,在thread2的while的一次判断之后下一次判断开始之前,thread1执行完任务调用unlock解锁,然后马上又申请锁调用lock,又将thread1.myNode的状态设置为true了,同时thread1将tail值设置为thread1.myPred(这个时候tail节点储存的是thread2.myNode的引用),如此一来2个线程就变成了一个相互等待的情况,即死锁。那么在unlock的时候执行了myNode.set(myPred.get());的话,现在的myNode和thread2的myPred已经不是一个对象了,所以thread2.myPred会因为第四行的qnode.locked=false;退出循环等待。个人拙见,这里myNode.set(myPred.get());替换成myNode.set(new QNode());效果是一样的。

        高山个人意见,出现死锁,是因为线程释放锁以后,迅速再获取锁,且释放锁加获取锁的动作,是在下一个线程的一个while循环之间完成。所以解决问题的根本方法,是避免多个线程关注同一块内存的情况。不论是myNode.set(myPred.get());还是myNode.set(new QNode()),都是让两个线程,关注不同的内存地址。用myNode.set(myPred.get()),可以复用对象,可能会比myNode.set(new QNode())效果稍微好一点。

CLHLock示意图

参考CLH lock queue的原理解释及Java实现 - Mr靖哥哥 - 博客园@ 背景 相信大部分人在看AQS的时候都能看到注释上有这么一段话: The wait queue is a variant of a "CLH" (Craig, Landin, ahttps://www.cnblogs.com/mrcharleshu/p/13338957.html

这里的死锁发生的情况有一定的特殊性,myNode myPred是ThreadLocall类型的,而在线程池的场景下为了线程复用Thread一旦被创建就不会销毁,所以ThreadLocal类型的变量使用完一定要手动清理(下次执行之前如果不手动清理,ThreadLocal类型的变量还是上一次执行的结果),上面的第五行代码其实也是ThreadLocal使用完清理变量的意思,如果不使用线程池的话即使没有第五行代码也不会死锁。

这段代码就会引起死锁(卡住不动),运行多次可重现

public class CLHLock {
    ...
    public void unlock() {
        QNode qnode = myNode.get();
        qnode.locked = false;
        //myNode.set(myPred.get());
    }
    ...
}

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    CLHLock lock = new CLHLock();

    Runnable runnable = new Runnable() {
        private int a;

        @Override
        public void run() {
            lock.lock();
            for (int i = 0; i < 100; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName() + " a = " + a);
            lock.unlock();
        }
    };

    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);
    executorService.execute(runnable);

    executorService.shutdown();
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值