java clh_CLH lock 原理及JAVA实现

CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。

CLH Lock摘要

CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.

The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

CLH锁是自旋锁的一种,对它的研究是因为AQS源代码中使用了CLH锁的一个变种,为了更好的理解AQS中使用锁的思想,所以决定先好好理解CLH锁

二. CLH原理

CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

三. Java代码实现

类图

e976194dba7ccf1105d47a67faa27bb5.png

public interface Lock {

void lock();

void unlock();

}

public class QNode {

volatile boolean locked;

}

import java.util.concurrent.atomic.AtomicReference;

public class CLHLock implements Lock {

// 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail

private final AtomicReference tail;

// 前驱节点,每个线程独有一个。

private final ThreadLocal myPred;

// 当前节点,表示自己,每个线程独有一个。

private final ThreadLocal myNode;

public CLHLock() {

this.tail = new AtomicReference(new QNode());

this.myNode = new ThreadLocal() {

protected QNode initialValue() {

return new QNode();

}

};

this.myPred = new ThreadLocal();

}

@Override

public void lock() {

// 获取当前线程的代表节点

QNode node = myNode.get();

// 将自己的状态设置为true表示获取锁。

node.locked = true;

// 将自己放在队列的尾巴,并且返回以前的值。第一次进将获取构造函数中的那个new QNode

QNode pred = tail.getAndSet(node);

// 把旧的节点放入前驱节点。

myPred.set(pred);

// 判断前驱节点的状态,然后走掉。

while (pred.locked) {

}

}

@Override

public void unlock() {

// unlock. 获取自己的node。把自己的locked设置为false。

QNode node = myNode.get();

node.locked = false;

myNode.set(myPred.get());

}

}

简单的看一下CLH的算法定义

the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

基于list,线程仅在一个局部变量上自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁结束.

所以在java中使用了ThreadLocal作为具体实现,AtomicReference为了消除多个线程并发对tail引用Node的影响,核心方法lock()中分为3个步骤去实现

初始状态 tail指向一个node(head)节点

private final AtomicReference tail = new AtomicReference(new Node());

thread加入等待队列: tail指向新的Node,同时Prev指向tail之前指向的节点,在java代码中使用了getAndSet即CAS操作使用

Node pred = this.tail.getAndSet(node);

this.prev.set(pred);

寻找当前线程对应的node的前驱node然后开始自旋前驱node的status判断是否可以获取lock

while (pred.locked);

同理unlock()方法,获取当前线程的node,设置lock status,将当前node指向前驱node(这样操作tail指向的就是前驱node等同于出队操作).至此CLH Lock的过程就结束了

测试CLHLock

public class CLHLockDemo2 {

public static void main(String[] args) {

final Kfc kfc = new Kfc();

for (int i = 0; i < 10; i++) {

new Thread("eat" + i) {

public void run() {

kfc.eat();

}

}.start();

}

}

}

class Kfc {

private final Lock lock = new CLHLock();

private int i = 0;

public void eat() {

try {

lock.lock();

System.out.println(Thread.currentThread().getName() + ": " + --i);

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

public void cook() {

try {

lock.lock();

System.out.println(Thread.currentThread().getName() + ": " + ++i);

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

lock.unlock();

}

}

}

运行结果

eat1: -1

eat0: -2

eat3: -3

eat4: -4

eat7: -5

eat2: -6

eat5: -7

eat6: -8

eat8: -9

eat9: -10

四. CLH优缺点

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。

五. 了解与CLH对应的MCS自旋锁

MCS 自旋锁

MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。

MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {

public static class MCSNode {

volatile MCSNode next;

volatile boolean isWaiting = true; // 默认是在等待锁

}

volatile MCSNode queue;// 指向最后一个申请锁的MCSNode

private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater

.newUpdater(MCSLock.class, MCSNode.class, "queue");

public void lock(MCSNode currentThread) {

MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1

if (predecessor != null) {

predecessor.next = currentThread;// step 2

while (currentThread.isWaiting) {// step 3

}

} else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁

currentThread.isWaiting = false;

}

}

public void unlock(MCSNode currentThread) {

if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义

return;

}

if (currentThread.next == null) {// 检查是否有人排在自己后面

if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4

// compareAndSet返回true表示确实没有人排在自己后面

return;

} else {

// 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者

// 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完

while (currentThread.next == null) { // step 5

}

}

}

currentThread.next.isWaiting = false;

currentThread.next = null;// for GC

}

}

MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。

CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。

CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。

小结

CLH Lock是一种比较简单的自旋锁算法之一,因为锁的CAS操作涉及到了硬件的锁定(锁总线或者是锁内存)所以性能和CPU架构也密不可分,该兴趣的同学可以继续深入研究包括MCS锁等。CLH Lock是独占式锁的一种,并且是不可重入的锁,这篇文章是对AQS锁源代码分析的预热篇

参考内容:

https://segmentfault.com/a/1190000007094429

https://blog.csdn.net/faicm/article/details/80501465

https://blog.csdn.net/aesop_wubo/article/details/7533186

https://www.jianshu.com/p/0f6d3530d46b

https://blog.csdn.net/jjavaboy/article/details/78603477

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值