【算法数据结构专题】「线程锁算法专项」初探CLH队列锁机制原理分析

news2024/11/14 10:23:56

技术扩展

SMP(对称多处理器架构)

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

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

NUMA(非一致性内存访问)

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

  • 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架构

加锁逻辑

  1. 获取当前线程的锁节点,如果为空,则进行初始化;

  2. sync方法获取链表的尾节点,并将当前节点置为尾节点,此时原来的尾节点为当前节点的前置节点

  3. 如果尾节点为空,表示当前节点是第一个节点,直接加锁成功

  4. 如果尾节点不为空,则基于前置节点的锁值(locked==true)进行自旋,直到前置节点的锁值变为false

解锁逻辑

  1. 获取当前线程对应的锁节点,如果节点为空或者锁值为false,则无需解锁,直接返回

  2. 【sync方法为尾节点赋空值,赋值不成功表示当前节点不是尾节点,则需要将当前节点的locked=false解锁节点】。如果当前节点是尾节点,则无需为该节点设置

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

CLHLock的类图如下所示

简易代码

// 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即可。

CLH优缺点

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/97457.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Python互转pdf文档和word文档

1 前言 一日&#xff0c;欲将手头上的一份pdf文档转换成word文档。先试着用XX办公软件试了下&#xff0c;微信扫码登录后&#xff0c;在PDF转换界面&#xff0c;选中文档&#xff0c;点击“开始转换”&#xff0c;弹出提示对话框&#xff1a;免费的只给转换5页文档&#xff0c…

十万部冷知识:本届世界杯阿根廷会夺冠吗?

明天&#xff0c;世界杯的决赛“阿法大战”就开始了。而我个人是希望阿根廷夺冠的&#xff0c;熟悉我的人会知道&#xff0c;在2014巴西世界杯的64场比赛中&#xff0c;我曾预测对了63场&#xff0c;唯一一场不对的就是&#xff0c;那年阿根廷和德国的决赛&#xff0c;我当时就…

基于SSM村委会工作管理系统

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 工作人员&#xff1a; (1)通知管理&#xff1a;对日常的重要信息以及公告进行发布通知。 (2)财务管理&#xff1a;用于收…

【学习打卡】CAM可解释性分析-算法讲解

文章目录引言该算法能做什么直观上研究上意义上精妙之处定位特点弱监督学习特点作者简介CAM算法原理具体计算方法巧妙之处讨论全卷积神经网络池化简介池化的作用全局平均池化CAM总结SqueezeNet显著性分析的意义例1&#xff1a;工艺改进例2&#xff1a;识别鸟例3&#xff1a;围棋…

事业编招聘:事业单位招聘136人!可免笔试!

吉林的小伙伴看过来 2023年吉林四平事业编招聘136人 本科起报名&#xff0c;研究生免除笔试 报名时间&#xff1a;12月20日至12月24日 感兴趣的宝子们别错过了哦 为深入实施“万名大学生留平计划”&#xff0c;持续加大我市党政干部的专业化人才储备力度&#xff0c;现面向…

数据治理的数据流程整合

一、核心业务流程 在企业业务整合时&#xff0c;根据企业对信息化的投入&#xff0c;避免整合对企业业务流程影响过大&#xff0c;按照循序渐进的方式进行整合。 核心业务流程是企业经营、存在、发展的基础。在信息整合中&#xff0c;要围绕这样的业务流程整合企业的信息。在…

首看世界杯

首看世界杯&#xff0c;不谈技术&#xff0c;只聊自己的几点感受&#xff0c;纯属个人感想。 今年是第一次关注世界杯&#xff0c;本来对足球是没有什么兴趣的。如果说对足球有什么了解的话&#xff0c;大部分还是来自小时候的动画片“足球小将”。但是看现实中的足球比赛&…

Java项目:SSM酒吧后台管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 管理员角色包含以下功能&#xff1a; 管理员登录,桌位信息查看,查看账单,日常维护,酒水库存管理等功能。 环境需要 1.运行环境&#xff1a;最…

桌面壁纸实时展示粉丝数(CSDN)

最近csdn偶尔就又有几个同学关注我&#xff0c;觉得很有动力&#xff01;于是我想能在任何时候的桌面壁纸&#xff0c;都能看到csdn粉丝数以及显示他们的昵称&#xff0c;我觉得会很有意义&#xff01; 下面展示效果&#xff0c;(&#xff62;&#xff65;ω&#xff65;)&…

ArcGIS:如何进行栅格数据的拼接和裁剪、坡度坡向的提取、地形透视图的建立、等高线的提取、剖面图的创建?

目录 01 说明 02 实验目的及要求 03 实验设备及软件平台 04 实验内容与步骤 4.1 DEM 数据拼接和裁剪 4.2 地形属性的提取 4.3 透视图的建立&#xff08;均在ArcScence中操作&#xff09; 4.4 建立和显示 TIN 4.5 创建等高线图层 4.6 垂直剖面图创建 4.7 坡度分级 05 实验结果与…

【MySQL】MySQL性能优化

MySQL性能优化1、SQL语句及索引优化1.1 EXPLAIN查看索引使用情况1.2 SQL语句中IN包含的值不应过多1.3 SELECT语句务必指明字段名称1.4 当只需要一条数据的时候&#xff0c;使用limit 1&#xff0c;limit 是可以停止全表扫描的1.5 排序字段加索引1.6 如果限制条件中其他字段没有…

JAVA类和对象重点笔记及理解

1.类创建对象的详细过程 创建完成&#xff0c;dog就成了一个实例&#xff08;对象&#xff09;&#xff0c;具有属性和方法 Dog类的属性&#xff1a;一般叫做成员变量 Dog类的方法&#xff1a;一般叫做成员方法 类是对象的抽象&#xff0c;对象是类的具体实例。 2.JAVA的数据…

Mycat(4):mycat名词解释

1、逻辑库 对实际应用来说&#xff0c;并不需要知道中间件的存在&#xff0c;业务开发人员只需要知道数据库的概念&#xff0c;所以数据库中间件可以被看做是一个或多个数据库集群构成的逻辑库。 如图一中&#xff0c;在MYCAT服务区中的TESTDB库&#xff0c;只是逻辑上存在的数…

使用GraalVM 构建 Spring Boot 3.0 原生可执行文件

GraalVM 介绍 既然是VM&#xff0c;那肯定也是一个虚拟机&#xff0c;那它跟JVM有关系吗&#xff1f;有一定关系&#xff0c;GraalVM 可以完全取代上面提到的那几种虚拟机&#xff0c;比如 HotSpot。把你之前运行在 HotSpot 上的代码直接平移到 GraalVM 上&#xff0c;不用做任…

【GRU回归预测】基于麻雀算法优化门控循环单元SSA-GRU神经网络实现多输入单输出回归预测附matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

java秋招被问到的八股文

投递的岗位是Java后端开发&#xff0c;八股文背了很多&#xff0c;现在面过三十场&#xff0c;做一个总结&#xff0c;整理出一些我确实被问到的问题。 该准备什么 首先是简历里的项目&#xff0c;一定要好好整理&#xff0c;项目的架构是怎样的&#xff0c;开发过程中遇到的…

国产实时操作系统+intel x86/龙芯平台超边缘计算机方案

引言 近年来&#xff0c;物联网、云计算、机器学习和网络安全等技术不断推动工业 4.0 的发展&#xff0c;“云边端” 的架构正逐步替代 “云管端”&#xff0c;边缘计算成为新时代许多领域转型的关键要素。以智能制造为例&#xff0c;不同于为互联网服务的 CDN 边缘计算技术&a…

Spring(二):Spring的创建和使用

目录一、创建Spring项目1.1 创建一个Maven项目1.2 添加Spring框架支持1.3 创建启动类二、使用Spring存储对象2.1 创建Bean2.2 将Bean注册到容器三、获取并使用Bean对象3.1 创建Spring的上下文3.2 从Spring上下文对象中取出Bean对象3.3 使用Bean一、创建Spring项目 1.1 创建一个…

传统数据治理的常见陷阱有哪些?

一、传统的数据治理 传统的数据治理是一种数据优先的治理方法。这种传统方法缺乏响应数据用户需求的流动性——或者在新法规出现时适应新法规的灵活性。传统方法概述角色、创建数据标准、分配责任并创建公司范围的数据策略。因为它强调对数据的控制&#xff0c;这种方法威胁工…

Java程序接入ChatGPT

Java程序接入ChatGPT0 前言1 还想体验的小伙伴可以试试2 Java接入前准备3 官方支持接入语言4 调用费用5 接口调用说明6 代码实现6.1 postman调用6.2 Java调用7 小结0 前言 之前文章中我们聊过怎么注册使用最近很火的ChatGPT&#xff1f;这期我们来看看怎么在Java中调用ChatGPT…