AQS为什么用双向链表?
首先,在AQS中,等待队列是通过Node类来表示的,每个Node节点包含了等待线程的信息以及等待状态。下面是Node类的部分源码:
static final class Node {// 等待状态volatile int waitStatus;// 前驱节点volatile Node prev;// 后继节点volatile Node next;// 等待线程volatile Thread thread;// 等待条件Node nextWaiter;// ...省略其他代码...
}
从上面的代码可以看出,每个Node节点都有一个指向前驱节点和后继节点的指针,这样可以在O(1)时间内查找前驱和后继节点。
接下来,我们来看看AQS是如何使用双向链表来管理等待队列的。AQS内部有一个成员变量volatile Node head,它表示等待队列的头节点。当一个线程需要等待锁或条件时,它会创建一个Node节点并插入到等待队列的尾部。这个过程是通过以下方法实现的:
private Node addWaiter(Node mode) {// 创建一个Node节点,表示当前线程Node node = new Node(Thread.currentThread(), mode);// 尝试通过CAS操作将Node节点插入到等待队列的尾部Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;
}
在上面的代码中,首先创建了一个Node节点表示当前线程,然后尝试通过CAS操作将它插入到等待队列的尾部。如果CAS操作失败,说明有其他线程正在修改等待队列,此时会调用enq()方法来将节点插入到队列中。
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // 如果队列为空,需要先初始化队列if (compareAndSetHead(new Node()))tail = head;} else { // 如果队列不为空,将节点插入到队列尾部node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
在enq()方法中,首先获取等待队列的尾节点,如果尾节点为空,说明队列还没有初始化,需要先创建一个空的头节点来初始化队列。如果尾节点不为空,就将新的节点插入到尾节点的后面。如果CAS操作失败,说明有其他线程正在修改等待队列,这时需要重新尝试。
当一个线程持有锁的线程释放锁时,它会将等待队列的头节点出队并唤醒它的后继节点,这个过程是通过以下方法实现的:
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);Node s = node.next;if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread);
}
在上面的代码中,首先将头节点设置为当前节点,然后将头节点的prev指针置为null,表示它已经出队了,将头节点的thread指针置为null,表示当前线程不再持有锁。接下来,通过unparkSuccessor()方法唤醒后继节点。这个方法中,首先检查当前节点的等待状态,如果它的等待状态小于0,说明它是一个被取消的节点,将它的等待状态置为0。然后,找到当前节点的后继节点,如果它不存在或者它的等待状态大于0,说明它不能被唤醒,这时就需要从等待队列的尾部开始往前找,找到一个等待状态小于等于0的节点来唤醒。最后,通过LockSupport.unpark()方法唤醒后继节点的线程。
从双向链表的特性来看,我认为AQS使用双向链表有三个方面的考虑。
第一个方面,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,
这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。
所以线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从head节点开始遍历,性能非常低。

第二个方面,在Lock接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。
也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。
这个时候,被中断的线程的状态会修改成CANCELLED。
被标记为CANCELLED状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。
意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。
在这种情况下,如果是单向链表,就需要从Head节点开始往下逐个遍历,找到并移除异常状态的节点。
同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争。

第三个方面,为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。
但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。
否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。
所以为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。
所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。

相关文章:

AQS为什么用双向链表?
首先,在AQS中,等待队列是通过Node类来表示的,每个Node节点包含了等待线程的信息以及等待状态。下面是Node类的部分源码:static final class Node {// 等待状态volatile int waitStatus;// 前驱节点volatile Node prev;// 后继节点…...
AtCoder Beginner Contest 292——A-E题讲解
蒟蒻来讲题,还望大家喜。若哪有问题,大家尽可提! Hello, 大家好哇!本初中生蒟蒻讲解一下AtCoder Beginner Contest 292这场比赛的A-E题! A题 原题 Problem Statement You are given a string SSS consisting of lo…...

(蓝桥真题)最长不下降子序列(权值线段树)
样例输入: 5 1 1 4 2 8 5 样例输出: 4 分析:看到这种对其中连续k个数进行修改的我们就应该想到答案是由三部分组成,因为求的是最长不下降子序列,那么我们可以找到一个最合适的断点i,使得答案是由区间[1…...
数据类型及参数传递
1.数据类型 java中的基本数据类型: 数值型: 整数型:byte short long int 浮点型:float double 布尔型: boolean字符串: char java中的引用数据类型: 数组(array) 类(class…...

永春堂1300系统开发|解析永春堂1300模式商城的五大奖项
电商平台竞争越来越激烈,各种营销方式也是层出不穷,其中永春堂1300营销模式,以其无泡沫和自驱动性强等特点风靡一时。在这套模式中,虽然单型价格差异较大,但各种奖励的设计,巧妙的兼顾了平台和所有会员的利…...

最近一年我都干了什么——反思!!
过去一年不管是学习方式还是心态上都和以往有了许多不同的地方,比较昏昏沉沉。最近慢慢找到状态了,就想赶紧记录下来。 学习 在学习新技术的过程中开始飘了,总感觉有了一些开发经验后就觉得什么都不用记,知道思路就行遇到了现场百…...
Docker学习(十七)save 和 export 命令的区别
Docker 中有两个命令可以将镜像导出为本地文件系统中的 tar 文件:docker save 和 docker export。尽管它们的作用类似,但它们之间有一个重要的区别。 1.使用方式的不同: docker save 的使用示例: docker save -o test.tar image…...

【数据结构初阶】详解“树”
目录 前言 1.树概念及结构 (1)树的概念 (2)树的名词介绍 (3)树的表示 编辑 2.二叉树概念及结构 (1)概念 (2)特殊的二叉树 (3࿰…...

20230304 CF855 div3 vp
Dashboard - Codeforces Round 855 (Div. 3) - Codeforces呃呃,评价是,毫无进步呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训…...

UML 时序图
时序图(Sequence Diagram)是显示对象之间交互的图,是按时间顺序排列的。 时序图中显示的是参与交互的对象及其对象之间消息交互的顺序。 时序图包括的建模元素主要有:对象(Actor)、生命线(Lif…...

详解进程 及 探查进程
进程的概念PCB是什么task_struct的作用如何执行进程进程的探查什么是bashps命令的使用(查看进程)创建进程探究父子进程进程的概念 简而言之,进程就是正在在执行的程序 之前说过,程序执行的第一步Windows是双击程序Linux是 ./ &a…...
汇编相关问题
汇编语言期末复习题DX:单项选择题 DU:多项选择题 TK:填空题 MC:名词解释 v JD:简答题 CXFX:程序分析题 CXTK:程序填空题 BC:编程题第1章:基础知识1、在汇编语言程序的开发…...
华为OD机试Golang解题 - 火星文计算 2 | 包含思路
华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典文章目录 华为Od必看系列使用说明本期题目…...
成功解决configure: error: the HTTP rewrite module requires the PCRE library
文章目录 前言问题复现问题解决思考环节总结前言 大家好,我是沐风晓月,本专栏是记录日常实验中的所有疑难杂症,教程的安装,程序的bug,甚至各类报错,如果你也遇到了困惑和问题,欢迎与我一起交流学习。 另外不要解决完问题就结束了,思考环节也要好好看看哦。 问题复现…...
UNIX--GDB调试
通常,在为调试而编译时,我们会关掉编译器优化选项(-O),并打开调试选项(-g)。另外,-Wall 在尽量不影响程序行为的情况下选项打开所有 warning,也可以发现许多问题,避免一些不必要的 BUG。 GDB 命令-启动、退…...
孤单数算法
1.背景 腾讯终面:孤单的QQ号码怎么找? 问题一:有n个QQ号码,除1个孤单的QQ号码外,其余的QQ号码都是成双成对的,求这个孤单的QQ号码,要求:时间复杂度为O(n), 空间复杂度为O(1). 问题…...
triangulate_object_model_3d算子总结
目录 1.去掉固定方向的点云干扰 2.增加八叉树深度,实现更高细节级别的三角测量 3.腐蚀和膨胀,得到更平滑的点云 1.去掉固定方向的点云干扰 例程:triangulate_object_model_3d_xyz_mapping.hdev...

ZincSearch Java 客户端教程
ZincSearch Zinc 简单、强大,不了解的同学可以参见我之前的博客。今天我们这里谈谈 Java 环境如何集成 Zinc 客户端,跟如何使用的。 安装 Zinc 到 Github 的官方 Releases 下载: 我的是 Windows 开发环境,下载 zincsearch_0.4…...

数据结构(一)(嵌入式学习)
数据结构干货总结(一)基础线性表的顺序表示线性表的链式表示单链表双链表循环链表循环单链表循环双链表栈顺序存储链式存储队列队列的定义队列的常见基本操作队列的顺序存储结构顺序队列循环队列队列的链式存储结构树概念二叉树二叉树的创建基础 数据&a…...
合成复用原则-快速理解
什么是合成/聚合复用原则? 合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。 简述为:要尽量使用合成/聚合,尽量不要使用继承…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...

高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级
在互联网的快速发展中,高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司,近期做出了一个重大技术决策:弃用长期使用的 Nginx,转而采用其内部开发…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
智能AI电话机器人系统的识别能力现状与发展水平
一、引言 随着人工智能技术的飞速发展,AI电话机器人系统已经从简单的自动应答工具演变为具备复杂交互能力的智能助手。这类系统结合了语音识别、自然语言处理、情感计算和机器学习等多项前沿技术,在客户服务、营销推广、信息查询等领域发挥着越来越重要…...

基于 TAPD 进行项目管理
起因 自己写了个小工具,仓库用的Github。之前在用markdown进行需求管理,现在随着功能的增加,感觉有点难以管理了,所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD,需要提供一个企业名新建一个项目&#…...

如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...