Java AQS 核心数据结构-CLH 锁及优化

news2025/2/4 11:46:19

Java AQS 核心数据结构-CLH 锁

什么是CLH锁

CLH 锁是对自旋锁的一种改进,有效的解决了以上的两个缺点。

  • 第一个是锁饥饿问题。在锁竞争激烈的情况下,可能存在一个线程一直被其他线程”插队“而一直获取不到锁的情况。
  • 第二是性能问题。在实际的多处理上运行的自旋锁在锁竞争激烈时性能较差。

首先它将线程组织成一个队列,保证先请求的线程先获得锁,避免了饥饿问题。其次锁状态去中心化,让每个线程在不同的状态变量中自旋,这样当一个线程释放它的锁时,只能使其后续线程的高速缓存失效,缩小了影响范围,从而减少了 CPU 的开销。

CLH 锁数据结构很简单,类似一个链表队列,所有请求获取锁的线程会排列在链表队列中,自旋访问队列中前一个节点的状态。当一个节点释放锁时,只有它的后一个节点才可以得到锁。CLH 锁本身有一个队尾指针 Tail,它是一个原子变量,指向队列最末端的 CLH 节点。每一个 CLH 节点有两个属性:所代表的线程和标识是否持有锁的状态变量。当一个线程要获取锁时,它会对 Tail 进行一个 getAndSet 的原子操作。该操作会返回 Tail 当前指向的节点,也就是当前队尾节点,然后使 Tail 指向这个线程对应的 CLH 节点,成为新的队尾节点。入队成功后,该线程会轮询上一个队尾节点的状态变量,当上一个节点释放锁后,它将得到这个锁。

下面用图来展示 CLH 锁从获取到释放锁的全过程。

图片

  1. CLH 锁初始化时会 Tail 会指向一个状态为 false 的空节点,如图1所示。
  2. 当 Thread 1(下称 T1)请求获取锁时,Tail 节点指向 T1 对应的节点,同时返回空节点。T1 检查到上一个节点状态为 false,就成功获取到锁,可以执行相应的逻辑了,如图2所示。
  3. 当 Thread 2(下称 T2)请求获取锁时,Tail 节点指向 T2 对应的节点,同时返回 T1 对应的节点。T2检查到上一个节点状态为 True,无法获取到锁,于是开始轮询上一个节点的状态,如图3所示。
  4. 当 T1 释放锁时,会将状态变量置为 False,如图4所示。
  5. T2 轮询到检查到上一个节点状态变为 False,则获取锁成功,如图5所示。

CLH 锁 Java 实现解析

图片

1、节点中的状态变量为什么用 volatile 修饰?可以不用 volatile 吗?

使用 volatile 修饰状态变量不是为了利用 volatile 的内存可见性,因为这个状态变量只会被持有该状态变量的线程写入,只会被队列中该线程的后驱节点对应的线程读,而且后者会轮询读取。因此,可见性问题不会影响锁的正确性。以上面的例子为例,T2 会不断轮询T1的状态变量,T1 将它的状态变更为 False 时 T2 没有立即感知也没有关系。该状态变量最终会写回内存并被 T2 终感知到变更后的值。

但要实现一个可以在多线程程序中正确执行的锁,还需要解决重排序问题。在《Java 并发编程实战》一书对于重排序问题是这么描述的:在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。对于 Java synchronized 关键字提供的内置锁(又叫监视器),Java Memory Model(下称 JMM)规范中有一条 Happens-Before(先行发生)规则:“一个监视器锁上的解锁发生在该监视器锁的后续锁定之前”,因此 JVM 会保证这条规则成立。

而自定义互斥锁就需要自己保证这一规则的成立,因此上述代码通过 volatile 的 Happens-Before(先行发生)规则来解决重排序问题。JMM 的 Happens-Before(先行发生)规则有一条针对 volatile 关键字的规则:“volatile 变量的写操作发生在该变量的后续读之前”。

2、CLH 锁是一个链表队列,为什么 Node 节点没有指向前驱或后继指针呢?

CLH 锁是一种隐式的链表队列,没有显式的维护前驱或后继指针。因为每个等待获取锁的线程只需要轮询前一个节点的状态就够了,而不需要遍历整个队列。在这种情况下,只需要使用一个局部变量保存前驱节点,而不需要显式的维护前驱或后继指针。

3、this.node.set(new Node()) 这行代码有何意义?

如果没有这行代码,Node 可能被复用,导致死锁,如下图所示:

图片

CLH 优缺点分析

CLH 锁作为自旋锁的改进,有以下几个优点:

  1. 性能优异,获取和释放锁开销小。CLH 的锁状态不再是单一的原子变量,而是分散在每个节点的状态中,降低了自旋锁在竞争激烈时频繁同步的开销。在释放锁的开销也因为不需要使用 CAS 指令而降低了。
  2. 公平锁。先入队的线程会先得到锁。
  3. 实现简单,易于理解。
  4. 扩展性强。下面会提到 AQS 如何扩展 CLH 锁实现了 j.u.c 包下各类丰富的同步器。

当然,它也有两个缺点:第一是因为有自旋操作,当锁持有时间长时会带来较大的 CPU 开销。第二是基本的 CLH 锁功能单一,不改造不能支持复杂的功能。

AQS 对 CLH 队列锁的改造

针对 CLH 的缺点,AQS 对 CLH 队列锁进行了一定的改造。

针对第一个缺点,AQS 将自旋操作改为阻塞线程操作

针对第二个缺点,AQS 对 CLH 锁进行改造和扩展,原作者 Doug Lea 称之为“CLH 锁的变体”。下面将详细讲 AQS 底层细节以及对 CLH 锁的改进。

AQS 中的对 CLH 锁数据结构的改进主要包括三方面:扩展每个节点的状态显式的维护前驱节点和后继节点以及诸如出队节点显式设为 null辅助 GC 的优化。正是这些改进使 AQS 可以支撑 j.u.c 丰富多彩的同步器实现。

扩展每个节点的状态

volatile int waitStatus;

AQS 同样提供了该状态变量的原子读写操作,但和同步器状态不同的是,节点状态在 AQS 中被清晰的定义,如下表所示:

状态名描述
SIGNAL表示该节点正常等待
PROPAGATE应将 releaseShared 传播到其他节点
CONDITION该节点位于条件队列,不能用于同步队列节点
CANCELLED由于超时、中断或其他原因,该节点被取消
显式的维护前驱节点和后继节点

上文我们提到在原始版本的 CLH 锁中,节点间甚至都没有互相链接。但是,通过在节点中显式地维护前驱节点,CLH 锁就可以处理“超时”和各种形式的“取消”:如果一个节点的前驱节点取消了,这个节点就可以滑动去使用前面一个节点的状态字段。对于通过自旋获取锁的 CLH 锁来说,只需要显式的维护前驱节点就可以实现取消功能,如下图所示:

volatile Node prev;
volatile Node next;

图片

是在 AQS 的实现稍有不同。因为 AQS 用阻塞等待替换了自旋操作,线程会阻塞等待锁的释放,不能主动感知到前驱节点状态变化的信息。AQS 中显式的维护前驱节点和后继节点,需要释放锁的节点会显式通知下一个节点解除阻塞,如下图所示,T1 释放锁后主动唤醒 T2,使 T2 检测到锁已释放,获取锁成功。

图片

其中需要关注的一个细节是:由于没有针对双向链表节点的类似 compareAndSet 的原子性无锁插入指令,因此后驱节点的设置并非作为原子性插入操作的一部分,而仅是在节点被插入后简单地赋值。在释放锁时,如果当前节点的后驱节点不可用时,将从利用队尾指针 Tail 从尾部遍历到直到找到当前节点正确的后驱节点。

辅助 GC

JVM 的垃圾回收机制使开发者无需手动释放对象。但在 AQS 中需要在释放锁时显式的设置为 null,避免引用的残留,辅助垃圾回收。

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

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

相关文章

6.1810: Operating System Engineering 2023 <Lab5: cow: Copy-on-write fork>

一、本节任务 二、要点 2.1 设备驱动(device driver) memory-mapped I/O:设备拥有一个地址范围,软件可以使用 ld/st 指令来访存从而读写设备的寄存器。平台设计者决定设备在物理内存空间中的位置。 内核如何识别设备中断&#…

K8s出现问题时,如何排查解决!

K8s问题的排查 1. POD启动异常、部分节点无法启动pod2. 审视集群状态3. 追踪事件日志4. 聚焦Pod状态5. 检查网络连通性6. 审视存储配置7. 研究容器日志8. K8S集群网络通信9. 问题:Service 是否通过 DNS 工作?10. 总结1、POD启动异常、部分节点无法启动p…

2024年度AI大模型趋势解读

文章目录 2024年度AI大模型趋势解读写在前面大模型时代大模型未来发展趋势总结 2024年度AI大模型趋势解读 写在前面 大模型指具备超大规模预训练语料、拥有超千亿规模模型参数的深度学习模型。由美国开放人工智能研究中心(OpenAI)研发、基于大模型的人工…

python 面试题第一弹

1. 如何理解Python中的深浅拷贝 浅拷贝(Shallow Copy)创建一个新的对象,该对象的内容是原始对象的引用。这意味着新对象与原始对象共享相同的内存地址,因此对于可变对象来说,如果修改了其中一个对象,另一个…

31. Ajax

简介 AJAX 是 Asynchronous JavaScript And XML 的简称。直译为,异步的JS和XML。AJAX的实际意义是,不发生页面跳转、异步载入内容并改写页面内容的技术。AJAX也可以简单的理解为通过JS向服务器发送请求。 AJAX这门技术很早就被发明,但是直到…

Python 新规范 pyproject.toml 完全解析

多谢:thank Python从PEP 518开始引入的使用pyproject.toml管理项目元数据的方案。 该规范目前已经在很多开源项目中得以支持: Django 这个 Python 生态的顶级项目在 5 个月之前开始使用 pyproject.tomlPytest 这个 Python 生态测试框架的领头羊在 4 个…

二、基于图像和结构化数据多模态融合的回归预测网络【框图讲解+源码】

整理读研期间做的项目与日常小实验 本篇未完待续…代码部分整理后补充 0. 背景 实验室有一些材料的SEM(扫描电镜)图像、也有对应的组分信息(结构化数据,包含类别特征和连续的数值特征),以及对应的力学性能…

c++学习笔记(14)-类的对象占用内存

1、类的对象占用内存大小 很多C书籍中都介绍过,一个class对象需要占用多大的内存空间。最权威的结论是: 1. 非静态成员变量总和。 2. 加上编译器为了CPU计算,作出的数据对齐处理。 3. 加上为了支持虚函数,产生的额外负担。 1.1、…

【自然语言处理】扩展命名实体识别器(NER)以使用spaCy标记新实体

自我介绍 做一个简单介绍,酒架年近48 ,有20多年IT工作经历,目前在一家500强做企业架构.因为工作需要,另外也因为兴趣涉猎比较广,为了自己学习建立了三个博客,分别是【全球IT瞭望】,【…

用C/C++实现MSML协议栈的详细介绍

一、MSML协议简介 MSML(Media Server Markup Language)是一种基于XML的标记语言,用于控制媒体服务器。它是媒体服务器控制协议的一种,允许第三方应用与媒体服务器进行交互,实现对媒体流的创建、修改和释放等操作。MSM…

Elasticsearch可视化平台Kibana [ES系列] - 第498篇

历史文章(文章累计490) 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 全…

W3 Total Cache Pro v2.6.0 – WordPress 插件

W3 Total Cache Pro v2.6.0:优化WordPress性能的专业解决方案 一、引言 在数字化的世界中,网站性能对于用户体验和搜索引擎排名至关重要。WordPress作为全球最受欢迎的内容管理系统之一,提供了大量的插件来帮助网站所有者优化其性能。其中&a…

数字人平台哪家好!

数字人是指利用人工智能技术,通过计算机生成或模拟人类的外貌、声音、动作和表情等特征,从而创造出具有一定个性和情感的虚拟角色。数字人可以用于于娱乐、教育、营销、服务等多个领域,为用户提供更加丰富和互动的体验。 目前,市场…

『JavaScript』深入理解JavaScript字符串的基础操作与内置方法

📣读完这篇文章里你能收获到 学习了JavaScript中字符串的基本操作掌握了JavaScript字符串的多种内置方法了解了JavaScript中的模板字符串和插值表达式熟悉了如何在JavaScript中使用正则表达式与字符串方法结合 文章目录 一、基本操作1. 创建字符串2. 字符串长度3. …

Java架构师系统架构设计实践

目录 1 导语2 架构设计实践本章概述3 架构设计要素概述和规划4 架构设计模式5 架构设计输入6 架构设计输出7 架构设计要素总结 想学习架构师构建流程请跳转:Java架构师系统架构设计 1 导语 Java架构师在进行系统架构设计时,需要综合考虑多个方面&#…

洛谷 P2367 语文成绩 刷题笔记

P2367 语文成绩 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 差分 令a[i]为b[i]数组的前缀和 a[n]b[1]b[2]b[3].....b[n]; a[n-1]b[1]b[2]b[3].....b[n-1]; 构造差分数组 b[i]a[i]-a[i-1]; 有什么好处 当我们想对a[l]--a[r]范围内所有数据加上一个数x 不必循环 for(i…

C++力扣题目150--逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式。返回一个表示表达式值的整数。 注意: 有效的算符为 、-、* 和 / 。每个操作数(运算对象)都可以是一个整数或者另一个表达式。两个…

前端---html 的基本结构

1. 结构代码 <!DOCTYPE html> <html><head> <meta charset"UTF-8"><title>网页标题</title></head><body>网页显示内容</body> </html>第一行<!DOCTYPE html>是文档声明, 用来指定页…

C# 使用xUnit进行单元测试

目录 写在前面 操作步骤 执行结果 写在前面 xUnit.net 是 NUnit 的作者Jim Newkirk搞的一个新的单元测试框架&#xff0c;目的是解决 NUnit 存在的一些问题和缺点&#xff0c;并在框架中加入一些最佳实践和扩展能力。 主要的改进点如下&#xff1a; 为每个测试方法产生一个…

漏洞复现-泛微OA xmlrpcServlet接口任意文件读取漏洞(附漏洞检测脚本)

免责声明 文章中涉及的漏洞均已修复&#xff0c;敏感信息均已做打码处理&#xff0c;文章仅做经验分享用途&#xff0c;切勿当真&#xff0c;未授权的攻击属于非法行为&#xff01;文章中敏感信息均已做多层打马处理。传播、利用本文章所提供的信息而造成的任何直接或者间接的…