@

背景

相信大部分人在看AQS的时候都能看到注释上有这么一段话:

The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue.

为了更好的理解AQS中使用锁的思想,所以决定先好好理解CLH锁。

在网上能查到很多关于CLH的博客,但不如我想象中的那么全面,于是自己来整理一篇清晰点的。

原理解释

论文地址

CLH的作者:Craig, Landin, and Hagersten。

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.

我们能看到它是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。同时它也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

这个算法很妙的点在于,在一个CAS操作帮助下,所有等待获取锁的线程之下的节点轻松且正确地构建成了全局队列。等待中的线程正如队列中的节点依次获取锁。

接下来就说一下这个算法的Java实现。

Java代码实现



这里面贴出的代码是主要流程代码,详细代码在GitHub中。

定义QNode

CLH队列中的节点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。

public class QNode {

    volatile boolean locked;
}

定义Lock接口

public interface Lock {

    void lock();

    void unlock();
}

定义CLHLock

节点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些节点之间没有明显的next指针,而是通过myPred所指向的节点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个节点。

public class CLHLock implements Lock {
// 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail
private final AtomicReference<QNode> tail;
// 前驱节点,每个线程独有一个。
private final ThreadLocal<QNode> myPred;
// 当前节点,表示自己,每个线程独有一个。
private final ThreadLocal<QNode> myNode; public CLHLock() {
this.tail = new AtomicReference<>(new QNode());
this.myNode = ThreadLocal.withInitial(QNode::new);
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);
// 在等待前驱节点的locked域变为false,这是一个自旋等待的过程
while (pred.locked) {
}
// 打印myNode、myPred的信息
peekNodeInfo();
} @Override
public void unlock() {
// unlock. 获取自己的node。把自己的locked设置为false。
QNode node = myNode.get();
node.locked = false;
myNode.set(myPred.get());
}
}

使用场景

public class KFC {
private final Lock lock = new CLHLock();
private int i = 0; public void takeout() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": 拿了第" + ++i + "份外卖");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}

运行代码

public static void main(String[] args) {
final KFC kfc = new KFC();
Executor executor = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 35; i++) {
executor.execute(kfc::takeout);
}
}

代码输出

为什么输出这么多日志后面有解释。

thread-1 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-1: 拿了第1份外卖
thread-2 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-2: 拿了第2份外卖
thread-3 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-3: 拿了第3份外卖
thread-4 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-4: 拿了第4份外卖
thread-5 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-5: 拿了第5份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-1: 拿了第6份外卖
thread-2 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-2: 拿了第7份外卖
thread-3 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-3: 拿了第8份外卖
thread-4 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-4: 拿了第9份外卖
thread-5 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-5: 拿了第10份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-1: 拿了第11份外卖
thread-2 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-2: 拿了第12份外卖
thread-3 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-3: 拿了第13份外卖
thread-4 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-4: 拿了第14份外卖
thread-5 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-5: 拿了第15份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-1: 拿了第16份外卖
thread-2 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-2: 拿了第17份外卖
thread-3 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-3: 拿了第18份外卖
thread-4 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-4: 拿了第19份外卖
thread-5 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-5: 拿了第20份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-1: 拿了第21份外卖
thread-2 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-2: 拿了第22份外卖
thread-3 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-3: 拿了第23份外卖
thread-4 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-4: 拿了第24份外卖
thread-5 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-5: 拿了第25份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-1: 拿了第26份外卖
thread-2 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-2: 拿了第27份外卖
thread-3 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-3: 拿了第28份外卖
thread-4 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-4: 拿了第29份外卖
thread-5 acquire lock success. myNode(QNode_1_locked:true), myPred(QNode_6_locked:false)
thread-5: 拿了第30份外卖
----------------------------------------------------------------------------------------
thread-1 acquire lock success. myNode(QNode_2_locked:true), myPred(QNode_1_locked:false)
thread-1: 拿了第31份外卖
thread-2 acquire lock success. myNode(QNode_3_locked:true), myPred(QNode_2_locked:false)
thread-2: 拿了第32份外卖
thread-3 acquire lock success. myNode(QNode_4_locked:true), myPred(QNode_3_locked:false)
thread-3: 拿了第33份外卖
thread-4 acquire lock success. myNode(QNode_5_locked:true), myPred(QNode_4_locked:false)
thread-4: 拿了第34份外卖
thread-5 acquire lock success. myNode(QNode_6_locked:true), myPred(QNode_5_locked:false)
thread-5: 拿了第35份外卖

代码解释

CLHLock的加锁、释放锁过程

  • 当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁,然后线程对tail域调用getAndSet方法,使自己成为队列的尾部,同时返回旧的尾部节点作为其前驱引用myPred,然后该线程就在前驱节点的locked字段上自旋,直到前驱节点释放锁。

  • 当一个线程需要释放锁时,将当前节点的locked域设置为false,同时回收前驱节点。如下图所示,线程A需要获取锁,其myNode域为true,此时tail指向线程A的节点,然后线程B也加入到线程A后面,tail指向线程B的节点。然后线程A和B都在它的myPred域上旋转,一旦它的myPred节点的locked字段变为false,它就可以获取到锁。明显线程A的myPred locked值为false,此时线程A获取到了锁。

第一个使用CLHLock的线程自动获取到锁

初始状态的tail的值是一个新的QNode,locked的值是默认的false。后面新加入的QNode在获取锁的时候都把locked置为true后放入尾节点并成为前驱节点。

为什么使用ThreadLocal保存myNode和myPred?

因为每个线程使用lock方法的时候,将QNode绑定到当前线程上,等unlock操作的时候还可以获取到本线程之前调用lock方法里创建的QNode对象。

为什么tail要用AtomicReference修饰?

因为我们在把尾节点更新成当前节点并返回旧的尾节点作为前驱节点的时候,我们希望这个操作是原子性的,AtomicReference的getAndSet()方法正好能满足我们的需求。

unlock中的set操作怎么理解?

也就是这一段代码:

myNode.set(myPred.get());

它有以下几点影响:

  • 将当前node指向前驱node,lock方法中再也拿不到当前node的引用了。这样操作等于把当前node从链表头部删除(并不是被JVM回收,第二个线程的myPred还引用它)
  • 当前线程若要在unlock之后再次拿锁需重新排队(每个线程自己都维护了两个QNode,一个在释放锁的时候把当前node置为前驱node,另一个在lock方法的时候重新获取尾node作为前驱node)
  • 如果所有的任务都是由固定数量的线程池执行的话,你会看到所有的QNode的使用会形成一个环形链表(实际不是),在打印日志中可看到,日志“拿了第31份”和日志“拿了第1份”的myNode和myPred一样。

为什么要有myPred,不用行不行?

也就是代码改成这样:

public void lock() {
QNode node = myNode.get();
node.locked = true; // spin on pre-node
QNode pred = tail.getAndSet(node);
while (pred.locked) {
}
} public void unlock() {
QNode node = myNode.get();
node.locked = false;
}

答案肯定是不行啦。

假设有两个线程:T1 & T2,T1持有锁,T2等待T1释放锁。

这时候T1.node.locked为true,T2.node.locked也为true,tail变量指向T2.node,却T2正在pred.locked自旋。这里的pred也是T1.node。

现在T1开始释放锁(设置T1.node.locked为false)并且在T2抢占到锁之前再次获取锁,此时T1.node.locked再次变成true,但是此时的尾节点是T2.node,所以T1只好等待T2释放锁。而T2也在等待T1释放锁,死锁发生了。

再结合上面myNode.set(myPred.get())代码的解释,myPred变量提供了两点好处:

  • 防止死锁发生,释放锁的时候也就释放了当前节点的引用。
  • 等待队列中节点具有顺序性(看日志打印)可保证锁竞争公平,每个等待锁的线程都持有前驱节点的引用(getAndSet返回),n个线程最后有n+1的节点(有一个是初始tail的node),所有的节点按照顺序循环使用。借助于myPred在释放锁后若要再次拿锁需排队且排在最后。

CLH优缺点

CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个QNode,L个锁有L个tail)。

CLH的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的。一种解决NUMA系统结构的思路是MCS队列锁。

NUMA与SMP

SMP(Symmetric Multi-Processor),即对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。SMP的优点是能够保证内存一致性,缺点是这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,可能会导致CPU资源的浪费。常用的PC机就属于这种。

NUMA(Non-Uniform Memory Access)非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问NUMA的由来。NUMA优点是可以较好地解决原来SMP系统的扩展问题,缺点是由于访问远地内存的延时远远超过本地内存,因此当CPU数量增加时,系统性能无法线性增加。

最后

我的疑惑点都在上文叙述了,如果还有不清晰的地方,希望可以在评论区指出,谢谢

参考

A Hierarchical CLH Queue Lock

JAVA并发编程学习笔记之CLH队列锁

CLH lock 原理及JAVA实现

Why CLH Lock need prev-Node in java

CLH lock queue的原理解释及Java实现的更多相关文章

  1. Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁

    上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...

  2. CLH lock 原理及JAVA实现

    --喜欢记得关注我哟[shoshana]--​ 前记 JUC中的Lock中最核心的类AQS,其中AQS使用到了CLH队列的变种,故来研究一下CLH队列的原理及JAVA实现 一. CLH背景知识 SMP ...

  3. Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性)

    Java自定义注解源码+原理解释(使用Java自定义注解校验bean传入参数合法性) 前言:由于前段时间忙于写接口,在接口中需要做很多的参数校验,本着简洁.高效的原则,便写了这个小工具供自己使用(内容 ...

  4. (4.1)Spring MVC执行原理和基于Java的配置过程

    一.Spring MVC执行原理和基于Java配置的配置过程 (一)Spring MVC执行过程,大致为7步. 所有的请求都会经过Spring的一个单例的DispacherServlet. Dispa ...

  5. Atitit.加密算法 des  aes 各个语言不同的原理与解决方案java php c#

    Atitit.加密算法 des  aes 各个语言不同的原理与解决方案java php c# 1. 加密算法的参数::算法/模式/填充 1 2. 标准加密api使用流程1 2.1. Md5——16bi ...

  6. Spring MVC执行原理和基于Java的配置过程

    一.Spring MVC执行原理和基于Java配置的配置过程 (一)Spring MVC执行过程,大致为7步. 所有的请求都会经过Spring的一个单例的DispacherServlet. Dispa ...

  7. Atitit. Async await 优缺点 异步编程的原理and实现 java c# php

    Atitit. Async await 优缺点 异步编程的原理and实现 java c# php 1. async & await的来源1 2. 异步编程history1 2.1. 线程池 2 ...

  8. atitit.复合变量,也就是类似$$a的变量的原理与实现 java c#.net php js

    atitit.复合变量,也就是类似$$a的变量的原理与实现 java c#.net php js 1.1. 复合变量,也就是类似$$a的变量,它会进行两次的解释. 1 1.2. 数据库里面的复合变量1 ...

  9. Ticket Lock, CLH Lock, MCS Lock

    如果不用OS提供的mutex,我们该如何实现互斥锁?(不考虑重入的情况) 1. naive lock 最简单的想法是,搞一个volatile类型的共享变量flag,值可以是flase(无锁)或者tru ...

随机推荐

  1. centos7启用EPEL Repository

    1,下载库文件 http://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/e/epel-release-7-11.noarch.rpm 2,安装 r ...

  2. 变量分割技术、判别学习(discriminative learning method)

    基于模型的优化方法(model-based optimization method): 小波变换.卡尔曼滤波.中值滤波.均值滤波: 优点:对于处理不同的逆问题都非常灵活:缺点:为了更好的效果而采用各种 ...

  3. css-设置背景透明度

    实现透明的css方法通常有以下3种方式,以下是不透明度都为80%的写法: css3的opacity:x,x 的取值从 0 到 1,如opacity: 0.8 兼容性:IE6.7.8不支持,IE9及以上 ...

  4. mysql数据库——选择优化的数据类型

    选择更小的数据类型:一般情况下,应该尽量使用可以正确存储数据的最小数据类型.更小的数据类型通常更快,它们占用更少的磁盘.内存和CPU缓存,并且处理时需要的CPU周期也更少.但是要确保没有低估需要存储的 ...

  5. C语言100题集合005-删除一维数组中所有相同的数,使之只剩一个

    系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...

  6. Java入门(7)

    Java入门经典(第7版) 作者:罗格斯·卡登海德 对象抛出异常,以指出发生了异常,这些异常可以被其他对象或虚拟机捕获. 其他异常使用5个新的关键字在程序运行时进行处理:try,catch,final ...

  7. Android Google官方文档解析之——Device Compatibility

    Android is designed to run on many different types of devices, from phones to tablets and television ...

  8. 完美实现CSS垂直居中的11种方法

    在做前端项目时CSS的水平居中我们经常使用,但有时还会用到CSS垂直居中,对于小白来说这个就有些难度了,下面看一下我是如何实现的 本人前端小白,正在做一个小程序开发的项目,css样式调整搞的头都大了. ...

  9. python详细图像仿射变换讲解

    仿射变换简介 什么是放射变换 图像上的仿射变换, 其实就是图片中的一个像素点,通过某种变换,移动到另外一个地方. 从数学上来讲, 就是一个向量空间进行一次线形变换并加上平移向量, 从而变换到另外一个向 ...

  10. 常用DOS指令

    Windows的DOS命令,其实是Windows系统的cmd命令,它是由原来的MS-DOS系统保留下来的. ​MS-DOS称为微软磁盘操作系统,最开始从西雅图公司(蒂姆·帕特森)买过来 MS-DOS系 ...