什么是CLH队列锁

CLH锁其实就是一种基于逻辑队列非线程饥饿的一种自旋公平锁。当多线程竞争一把锁时,获取不到锁的线程,会排队进入CLH队列的队尾,然后自旋等待,直到其前驱线程释放锁。由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁。

自旋锁与互斥锁的区别

由于CLH锁是一种自旋锁,那么我们先来看看自旋锁是什么?

自旋锁说白了也是一种互斥锁,只不过没有抢到锁的线程会一直自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程不会进入休眠状态,而是一直消耗CPU判断是否有资格获取锁。因此自旋锁适用于锁占用时间短的场合。

这里,我们可以总结出,CLH队列锁,如果队列过长(等待锁的线程过多),势必会给CPU带来极大的压力,因此CLH锁的使用场景就被局限住了。

Java中ReentrantLock重入锁中的AQS队列排队策略,是基于CLH队列的一种变种实现。原始的CLH队列,一般用于实现自旋锁。而AQS队列的实现,是获取不到锁的线程,先进行一小段时间的自旋,然后进入Park挂起状态。【同样的,ZK中的分布式锁,也使用了类似方式,获取不到锁的线程值监听前一个节点】。因此AQS解决了CLH每个线程无限自旋的问题,应用场景也更加广泛。

这里谈到了自旋锁,那么我们也顺便说下互斥锁。这里的互斥锁说的是传统意义的互斥锁,就是多个线程并发竞争锁的时候,没有抢到锁的线程会进入休眠状态即sleep-waiting,当锁被释放的时候,处于休眠状态的一个线程会再次获取到锁。缺点就是这一些列过程需要线程切换,需要执行很多CPU指令,同样需要时间。如果CPU执行线程切换的时间比锁占用的时间还长,那么可能还不如使用自旋锁。因此互斥锁适用于锁占用时间长的场合。

CLH锁原理

  • 首先有一个尾节点指针,通过这个尾结点指针来构建等待线程的逻辑队列,当有新的节点加入队列时,尾节点指针会指向这个新加入的节点,并将原本的尾节点变为当前新加入节点的前驱节点。因此能确保线程线程先到先服务的公平性,尾指针可以说是构建逻辑队列的桥梁;此外这个尾节点指针是原子引用类型,避免了多线程并发操作的线程安全性问题;
  • 通过等待锁的每个线程在自己的某个变量上自旋等待,这个变量指向自己的前驱节点中的变量,通过不断地自旋,感知到前驱节点的变化后成功获取到锁。

CLH锁的优点

  1. 没有惊群效应。假设有1000个线程等待获取锁,锁释放后,只会通知队列中的第一个线程去竞争锁,避免了同时唤醒大量线程 在瞬间争抢CPU资源,避免了惊群效应。(此处仅仅是不会对锁过度的争抢,也就是公平锁的好处。但是自旋锁的实现方式依然消耗CPU)
  2. CLH队列锁的长处是空间复杂度低(假设有n个线程。L个锁,每一个线程每次仅仅获取一个锁,那么须要的存储空间是O(L+n),n个线程有n个myNode。L个锁有L个tail)。

CLH锁的缺点

在NUMA系统结构下性能稍差。在这样的系统结构下,每一个线程有自己的内存,假设前趋结点的内存位置比較远。自旋推断前趋结点的locked域,性能将大打折扣,在SMP结构下还是非常有效的。【CLH自旋在前驱节点上,访问的是其他线程的变量值,在NUMA架构下,其他线程变量有可能是对端CPU的高速缓存,因此更适合SMP架构】

深入CLH队列源码

那么,下面我们先来看CLH锁实现代码,然后通过一步一图来详解CLH锁。

// CLHLock.javapublic class CLHLock {/*** CLH锁节点*/private static class CLHNode {// 锁状态:默认为false,表示线程没有获取到锁;true表示线程获取到锁或正在等待// 为了保证locked状态是线程间可见的,因此用volatile关键字修饰volatile boolean locked = false;}// 尾结点,总是指向最后一个CLHNode节点// 【注意】这里用了java的原子系列之AtomicReference,能保证原子更新private final AtomicReference<CLHNode> tailNode;// 当前节点的前继节点private final ThreadLocal<CLHNode> predNode;// 当前节点private final ThreadLocal<CLHNode> curNode;// CLHLock构造函数,用于新建CLH锁节点时做一些初始化逻辑public CLHLock() {// 初始化时尾结点指向一个空的CLH节点tailNode = new AtomicReference<>(new CLHNode());// 初始化当前的CLH节点curNode = new ThreadLocal() {@Overrideprotected CLHNode initialValue() {return new CLHNode();}};// 初始化前继节点,注意此时前继节点没有存储CLHNode对象,存储的是nullpredNode = new ThreadLocal();}/*** 获取锁*/public void lock() {// 取出当前线程ThreadLocal存储的当前节点,初始化值总是一个新建的CLHNode,locked状态为false。CLHNode currNode = curNode.get();// 此时把lock状态置为true,表示一个有效状态,// 即获取到了锁或正在等待锁的状态currNode.locked = true;// 当一个线程到来时,总是将尾结点取出来赋值给当前线程的前继节点;// 然后再把当前线程的当前节点赋值给尾节点// 【注意】在多线程并发情况下,这里通过AtomicReference类能防止并发问题// 【注意】哪个线程先执行到这里就会先执行predNode.set(preNode);语句,因此构建了一条逻辑线程等待链// 这条链避免了线程饥饿现象发生CLHNode preNode = tailNode.getAndSet(currNode);// 将刚获取的尾结点(前一线程的当前节点)付给当前线程的前继节点ThreadLocal// 【思考】这句代码也可以去掉吗,如果去掉有影响吗?predNode.set(preNode);// 【1】若前继节点的locked状态为false,则表示获取到了锁,不用自旋等待;// 【2】若前继节点的locked状态为true,则表示前一线程获取到了锁或者正在等待,自旋等待while (preNode.locked) {System.out.println("线程" + Thread.currentThread().getName() + "没能获取到锁,进行自旋等待。。。");}// 能执行到这里,说明当前线程获取到了锁System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁!!!");}/*** 释放锁*/public void unLock() {// 获取当前线程的当前节点CLHNode node = curNode.get();// 进行解锁操作// 这里将locked至为false,此时执行了lock方法正在自旋等待的后继节点将会获取到锁// 【注意】而不是所有正在自旋等待的线程去并发竞争锁node.locked = false;System.out.println("线程" + Thread.currentThread().getName() + "释放了锁!!!");// 小伙伴们可以思考下,下面两句代码的作用是什么??CLHNode newCurNode = new CLHNode();curNode.set(newCurNode);// 【优化】能提高GC效率和节省内存空间,请思考:这是为什么?// curNode.set(predNode.get());}
}

CLH锁的初始化逻辑

通过上面代码,我们缕一缕CLH锁的初始化逻辑先:

首先定义了一个CLHNode节点(在代码中以内部类的形式呈现),里面有一个locked属性,表示线程线程是否获得锁,默认为false。false表示线程没有获取到锁或已经释放锁;true表示线程获取到了锁或者正在自旋等待。

这个locked也就是我们每个节点轮询判断前驱节点是否释放锁的关键变量。但我们知道这个变量是跨线程获取的,为了保证locked属性线程间可见,该属性被volatile修饰。

接下来,我们看CLHLock类中有三个重要的成员变量:

CLHLock有三个重要的成员变量尾节点指针tailNode,当前线程的前继节点preNode和当前节点curNode。其中tailNode是AtomicReference类型,目的是为了保证尾节点的线程安全性;此外,preNode和curNode都是ThreadLocal类型,即线程本地变量类型,可见这些节点都是线程级别的。用来保存每个线程的前驱CLHNode和当前CLHNode节点。

接着,到我们的构造方法,看看构造方法都干了些什么事。

给尾指针tailNode和当前节点curNode初始化一个locked状态为false的CLHNode节点,此时前继节点preNode存储的是null。配合上我们之前说的,当节点为false时表示线程没有获取到锁或已经释放锁,因此当新增节点时,必然会直接获取锁。

CLH加锁过程

我们再来看看CLH锁的加锁过程,下面再贴一遍加锁lock方法的代码:

第一步:CLHNode currNode = curNode.get();

由于在构造函数中对每一个线程的curNode都做了初始化,所以直接获取当前线程的当前节点curNode,并且每次获取的CLHNode节点的locked状态都为false;

第二步:currNode.locked = true;

设置当前节点的状态位true。表示等待锁或者正在执行锁。意图就是阻塞它的后继节点获取锁。

第三步:CLHNode preNode = tailNode.getAndSet(currNode);

使用尾节点tailNode,将当前节点线程安全的进入队列尾部,并且返回原本的尾节点。

第四步:predNode.set(preNode);

将当原本的尾节点,设置为当前节点的前驱节点。

第五步:轮循检测前驱节点locked的状态

当前驱节点检测变为了false时,就成功获取到了锁。这也说明locked这种跨线程变量,必须要使用voliate修饰保证可见性。

但是此处我们发现,每一个节点(线程)都在自旋获取锁。因此如果在任务较长较多的情况下,CLH锁对CPU的性能消耗不言而喻。因此在未来的AQS中,对此处进行了优化。

画图解释

第一个节点加入

我们假设线程A是第一个加入队列的线程,我们都知道它的前驱节点是一个null,那么如何给它的前驱节点赋值呢?答案就是tailNode,在初始化的时候对taileNode中的locked成员也设为了false。因此当线程A通过taileNode获取队列中第一个元素的前驱节点时,也不会获取为null。

经过代码分析,我们得出了第一个线程加入队列后的状态图。

第二个节点加入

此时,第二个线程B加入,我们的队列会发生什么变化呢?假设线程A一直持有锁不释放。当线程B调用lock方法进入队列时,队列发生了哪些变化。

第三个节点加入

后续的流程就和第二个节点加入流程一模一样了。

CLH解锁过程

我们再来看看CLH锁的加锁过程,下面再贴一遍解锁unlock方法的代码:

由于我们的加锁逻辑设置的非常巧妙,解锁逻辑反而非常简单明了了。

首先,获取当前线程的CLHNode。(为什么是当前线程?因为肯定是需要解锁的线程调用unlock方法,此处是一个逻辑对应关系)。然后将locked状态置为false即释放了锁;locked因为被volitile关键字修饰,此时后面自旋等待的线程的局部变量preNode.locked也为false,因此后面自旋等待的线程结束while循环即结束自旋等待,此时也获取到了锁。这一步骤也在异步进行着。

然后给当前线程的表示当前节点的线程本地变量重新赋值为一个新的CLHNode。这一步就很莫名其妙了,这么做的目的是什么呢?我们留到后面再说。

现在线程A释放锁,线程B获取锁的流程图如下:

解锁流程中莫名其妙的两行代码

就是这两行代码,看得人莫名其妙。那么这两行代码取消了会有什么问题么?别急,这就给你上个例子。

首先,假设队列里存储的都是不同的线程的节点,那么即时注销掉代码,当前线程释放锁,其他线程抢到锁的流程如下图所示:

这是没有任何问题的,而问题出现在下面这种场景:

此时,CLH队列锁中只有一个线程在反复的添加节点。那么效果就是线程A执行完毕执行unlock方法,紧接着由它再次执行lock方法。现在假设我们这两行莫名其妙的代码注销掉,来看看流程图会出现什么问题。

注销掉两行代码出现问题

第一步:线程A执行lock

此时状态如上图。

第二步:线程A执行unlock

第三步:线程A再次执行lock

接下来,就是问题出现的关键了。此时的获取锁中:CLHNode preNode = tailNode.getAndSet(currNode);这段代码,根据tailNode获取前驱节点,此时的当前线程的前驱节点和当前节点都代表着同一个节点。那么当每次默认给当前节点的locked赋值为true时,就会将原本前驱节点locked值为false的节点又改成true。好家伙,不用说,之后的轮循就会无限等待前驱节点的锁。

反应到图中的效果如下:

    此时,我们就发现死循环的问题了。说到底,就是因为taileNode指向了同一个节点导致的。那么该如何解决呢?

添加这两行代码解决问题

第一步:线程A执行unlock

我们可以看到,在添加这两行代码后,线程A的在队列中的状态图变成了这样:

诶?这样,操作一下,tailNode就不会再指向ThreadA中的当前节点了。

第二步:线程A再次执行lock

可以看到,再次执行CLHNode preNode = tailNode.getAndSet(currNode);时,获取到的前驱节点就是一个已经被弃用,但必然是false的节点。从而解决了我们连续向CLH队列添加相同线程节点出现的死循环问题。

优化GC减少内存空间的优化

我们知道。我们之前的那两行代码,就是为了让我们当前线程持有的一个不和后继节点重复的locked值为false的CLHNode节点。那么我们每次新建对象必然会给内存,GC带来消耗。此时已经存在的前驱节点必然locked值为false,正好让我们拿来继续利用,避免过多无用对象的创建。

小结

我们在的CLH队列其实属于我们学习AQS的前菜。但是只有深入研究后,才知道CLH存在什么问题(CLH每一个线程都是一个自旋锁,非常消耗CPU),以及AQS在CLH的基础上做了哪些优化。

我们可以看到公平锁就是最初的实现理念就是CLH队列。

AQS的前菜—详解CLH队列锁相关推荐

  1. AQS基础——多图详解CLH锁的原理与实现

    1 什么是自旋锁和互斥锁? 由于CLH锁是一种自旋锁,那么我们先来看看自旋锁是什么? 自旋锁说白了也是一种互斥锁,只不过没有抢到锁的线程会一直自旋等待锁的释放,处于busy-waiting的状态,此时 ...

  2. AQS(AbstractQuenedSynchronizer)详解

    AQS原理 AQS:AbstractQuenedSynchronizer抽象的队列式同步器.是除了java自带的synchronized关键字之外的锁机制. AQS的全称为(AbstractQueue ...

  3. MySQL锁、事务隔离级别、MVCC机制详解、间隙锁、死锁等

    一. 简介 1. 锁定义 锁是计算机协调多个进程或线程并发访问某一资源的机制. 在数据库中,除了传统的计算资源(如CPU.RAM.I/O等)的争用以外,数据也是一种供需要用户共享的资源.如何保证数据并 ...

  4. 详解数据库的锁机制及原理

    详解数据库的锁机制及原理 1.数据库锁的分类 2.行锁 共享锁(读锁S锁) 排他锁(写锁X锁) 更新锁 3.意向锁(IX/IS锁) 4.锁机制解释数据库隔离级别 5.元数据锁(MDL锁) 6.间隙锁 ...

  5. 05.抽象队列同步器AQS应用之Lock详解

    AQS应用之Lock Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列.条件队列.独占获取.共享获取等,而这个行为 ...

  6. 详解优先级队列priority_queue(应用+模拟实现)

    优先级队列的概念 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元 素 ...

  7. Java多线程系列(六):深入详解Synchronized同步锁的底层实现

    谈到多线程就不得不谈到Synchronized,很多同学只会使用,缺不是很明白整个Synchronized的底层实现原理,这也是面试经常被问到的环节,比如: synchronized的底层实现原理 s ...

  8. mysql 事务 注意 优化_MySQL入门详解——事务、锁、优化

    MySQL事务 MySQL 事务主要用于处理操作量大,复杂度高的数据.比如说,在一个商城系统中,用户执行购买操作,那么用户订单中应该加一条,库存要减一条,如果这两步由于意外只进行了其中一步那么就会发生 ...

  9. linux 驱动器发送信号,Linux设备驱动并发控制详解(自旋锁,信号量)

    转发:Linux设备驱动并发控制详解(自旋锁,信号量) 作者:jinhaijun 提交日期:2008-3-12 14:08:00 | 分类: | 访问量:144 link:http://www.emb ...

  10. 【学习笔记】抽象队列同步器AQS应用之BlockingQueue详解

    文章目录 什么是AQS框架 Aqs核心源码 基于aqs实现的锁 BlockingQueue ArrayBlockingQueue LinkedBlockingQueue DelayQueue Bloc ...

最新文章

  1. c语言N*N的二维数组,c语言高手帮个忙(请先看问题,好解答
  2. 树状数组 + 位运算 LA 4013 A Sequence of Numbers
  3. the future of real-time rendering hardware
  4. hessian无法获取连接_PPPoE拨号设置完成后无法上网解决方法【详解】
  5. HMI智能座舱自动化测试软件
  6. VSCode 过滤.meta 文件
  7. C#利用HttpClient获取微信Web扫描登录二维码
  8. 【Unity优化篇】| Unity3D场景 常用优化策略,遮挡剔除、层消隐距离技术 和 LOD多层次细节
  9. 【GNN】时空图网络 tensorflow 实现
  10. Python2020期末考试试题及答案
  11. 吵翻了!导师确认录取后却被学生放了鸽子,生气之余导师建了个“失信名单”挂到网上
  12. R 一行代码出Cox回归模型的表格
  13. 巧用位运算实现大小写转换
  14. 给童鞋萌康康关于代码块(static代码块)的小知识
  15. 深入学习VMware vSphere---基础知识
  16. 添加solidworks许可证服务器,SolidWorks许可服务器问题排查
  17. Firefox和IE浏览器清除缓存方法
  18. 产品经理的竞品分析报告入门
  19. 技嘉显卡性能测试软件,性能测试成绩总结_技嘉 GA-G1.Sniper B5_主板评测-中关村在线...
  20. [蓝桥杯2022初赛] 砍竹子

热门文章

  1. 老式十字锁自动碰锁,换锁芯
  2. 闭关的日子 好无聊。偷了半日闲去书店shopping一下
  3. 微信ipad 62 A16登录协议,小程序code 小程序sessionid 获取
  4. 点云外包矩形框(六面体)
  5. 如果我们遇上得州寒潮,会不会「悲剧」?
  6. 查询GPU时无进程运行,但是显存却被占用了
  7. 2D激光雷达运动畸变矫正_base里程计
  8. Manjaro安装教程
  9. oracle自动清理归档,Oracle rman 自动清理归档日志
  10. 基于CEP的量化交易平台建设