【数据结构之旅】「线程锁算法专项」引领你走进CLH队列锁机制原理世界

简介: 【数据结构之旅】「线程锁算法专项」引领你走进CLH队列锁机制原理世界

CLH算法指南


技术扩展


SMP(对称多处理器架构)


  • SMP(Symmetric Multi-Processor),即对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享

image.png


  • SMP优点是能够保证内存一致性,缺点是这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,可能会导致CPU资源的浪费。常用的PC机就属于这种



NUMA(非一致性内存访问)


  • NUMA(Non-Uniform Memory Access)非一致存储访问,  将CPU分为CPU模块,每个CPU模块由多个CPU组成, 并且具有独立的本地内存、 I/O 槽口等,模块之间可以通过互联模块相互访问  ,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来


image.png


  • NUMA优点是可以较好地解决原来SMP系统的扩展问题,缺点是由于访问远程内存的延时远远超过本地内存,因此当CPU数量增加时,系统性能无法线性增加


自旋锁和互斥锁

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


自旋锁


  • 自旋锁说白了也是一种互斥锁,只不过没有抢到锁的线程会一直自旋等待锁的释放,处于busy-waiting的状态,此时等待锁的线程不会进入休眠状态,而是一直忙等待浪费CPU周期。
  • 因此自旋锁适用于锁占用时间短的场合


互斥锁


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

CLH锁机制


CLH锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁,由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁,CLH是一种基于单向链表的高性能、能确保无饥饿性,提供先来先服务公平性的自旋锁

  • 申请加锁的线程通过前驱节点的变量进行自旋
  • 前置节点解锁后,当前节点会结束自旋,并进行加锁



CLH节点模型


  • CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。
  • 结点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。


CLH锁原理


  • 首先有一个尾节点指针,通过这个尾结点指针来构建等待线程的逻辑队列因此能确保线程线程先到先服务的公平性,因此尾指针可以说是构建逻辑队列的桥梁
  • 此外这个尾节点指针是原子引用类型,避免了多线程并发操作的线程安全性问题
  • 通过等待锁的每个线程在自己的某个变量上自旋等待,这个变量将由前一个线程写入。
  • 由于某个线程获取锁操作时总是通过尾节点指针获取到前一线程写入的变量,而尾节点指针又是原子引用类型,因此确保了这个变量获取出来总是线程安全的



CLH锁分析


  • 在SMP架构下,CLH更具有优势。
  • 在NUMA架构下,如果当前节点与前驱节点不在同一CPU模块下,跨CPU模块会带来额外的系统开销,而MCS锁更适用于NUMA架构


image.png

加锁逻辑


  1. 获取当前线程的锁节点,如果为空,则进行初始化;
  2. sync方法获取链表的尾节点,并将当前节点置为尾节点,此时原来的尾节点为当前节点的前置节点
  3. 如果尾节点为空,表示当前节点是第一个节点,直接加锁成功
  4. 如果尾节点不为空,则基于前置节点的锁值(locked==true)进行自旋,直到前置节点的锁值变为false



解锁逻辑


  1. 获取当前线程对应的锁节点,如果节点为空或者锁值为false,则无需解锁,直接返回
  2. 【sync方法为尾节点赋空值,赋值不成功表示当前节点不是尾节点,则需要将当前节点的locked=false解锁节点】。如果当前节点是尾节点,则无需为该节点设置

CLHLock上还有一个尾指针,始终指向队列的最后一个结点。


CLHLock的类图如下所示

image.png




简易代码


// CLHLock.java
public 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() {
           @Override
           protected CLHNode initialValue() {
               return new CLHNode();
           }
       };
       // 初始化前继节点,注意此时前继节点没有存储CLHNode对象,存储的是null
       predNode = 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());
   }
}
复制代码



代码流程


  1. 当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁
  2. 然后线程对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前趋的引用myPred。
  3. 然后该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁。
  4. 当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点

该算法也是空间有效的,如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail。


这种算法有一个缺点是在NUMA系统架构下性能表现很差,因为它自旋的locked是前驱线程的,如果内存位置较远,那么性能会受到损失。【但是在SMP这种cache一致性的系统架构上表现良好。】




流程说明


  • 线程A需要获取锁,其myNode域为true,些时tail指向线程A的结点,然后线程B也加入到线程A后面,tail指向线程B的结点。
  • 然后线程A和B都在它的myPred对象上旋转,一旦它的myPred结点的locked字段变为false,它就可以获取锁进行继续执行业务方法。
  • 明显线程A的myPred locked域为false,此时线程A获取到了锁,如下图所示。


从代码中可以看出lock方法中有一个while循环,这 是在等待前趋结点的locked域变为false,这是一个自旋等待的过程。unlock方法很简单,只需要将自己的locked域设置为false即可。

image.png


CLH优缺点


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

相关文章
|
1月前
|
机器学习/深度学习 数据采集 算法
短视频到底如何推荐的?深度剖析视频算法推送原理详细且专业的解读-优雅草卓伊凡-【01】短视频算法推荐之数据收集
短视频到底如何推荐的?深度剖析视频算法推送原理详细且专业的解读-优雅草卓伊凡-【01】短视频算法推荐之数据收集
94 12
短视频到底如何推荐的?深度剖析视频算法推送原理详细且专业的解读-优雅草卓伊凡-【01】短视频算法推荐之数据收集
|
3月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
282 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
2月前
|
算法 安全 大数据
【算法合规新时代】企业如何把握“清朗·网络平台算法典型问题治理”专项行动?
在数字化时代,算法推动社会发展,但也带来了信息茧房、大数据杀熟等问题。中央网信办发布《关于开展“清朗·网络平台算法典型问题治理”专项行动的通知》,针对六大算法问题进行整治,明确企业需落实算法安全主体责任,建立健全审核与管理制度,并对算法进行全面审查和备案。企业应积极自查自纠,确保算法合规透明,防范风险,迎接新机遇。
|
2月前
|
运维 NoSQL 算法
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
88 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
|
2月前
|
缓存 监控 算法
内网监控管理软件:PHP 语言队列算法揭秘
在数字化办公环境中,内网监控管理软件对企业的稳定运行和信息安全至关重要。本文深入介绍PHP中的队列算法及其在内网监控软件中的应用,包括监控数据收集、任务调度和日志记录等场景,通过代码示例展示其实现方法。队列算法可提高性能、保证数据顺序并实现异步处理,为企业提供高效的安全保障。
35 1
|
3月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
138 16
|
3月前
|
机器学习/深度学习 算法 PyTorch
深度强化学习中SAC算法:数学原理、网络架构及其PyTorch实现
软演员-评论家算法(Soft Actor-Critic, SAC)是深度强化学习领域的重要进展,基于最大熵框架优化策略,在探索与利用之间实现动态平衡。SAC通过双Q网络设计和自适应温度参数,提升了训练稳定性和样本效率。本文详细解析了SAC的数学原理、网络架构及PyTorch实现,涵盖演员网络的动作采样与对数概率计算、评论家网络的Q值估计及其损失函数,并介绍了完整的SAC智能体实现流程。SAC在连续动作空间中表现出色,具有高样本效率和稳定的训练过程,适合实际应用场景。
733 7
深度强化学习中SAC算法:数学原理、网络架构及其PyTorch实现
|
4月前
|
算法 Java 数据库
理解CAS算法原理
CAS(Compare and Swap,比较并交换)是一种无锁算法,用于实现多线程环境下的原子操作。它通过比较内存中的值与预期值是否相同来决定是否进行更新。JDK 5引入了基于CAS的乐观锁机制,替代了传统的synchronized独占锁,提升了并发性能。然而,CAS存在ABA问题、循环时间长开销大和只能保证单个共享变量原子性等缺点。为解决这些问题,可以使用版本号机制、合并多个变量或引入pause指令优化CPU执行效率。CAS广泛应用于JDK的原子类中,如AtomicInteger.incrementAndGet(),利用底层Unsafe库实现高效的无锁自增操作。
181 0
理解CAS算法原理
|
4月前
|
存储 人工智能 缓存
【AI系统】布局转换原理与算法
数据布局转换技术通过优化内存中数据的排布,提升程序执行效率,特别是对于缓存性能的影响显著。本文介绍了数据在内存中的排布方式,包括内存对齐、大小端存储等概念,并详细探讨了张量数据在内存中的排布,如行优先与列优先排布,以及在深度学习中常见的NCHW与NHWC两种数据布局方式。这些布局方式的选择直接影响到程序的性能,尤其是在GPU和CPU上的表现。此外,还讨论了连续与非连续张量的概念及其对性能的影响。
169 3
|
5月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
145 58
下一篇
oss创建bucket