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…...
合成复用原则-快速理解
什么是合成/聚合复用原则? 合成/聚合复用原则是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。 简述为:要尽量使用合成/聚合,尽量不要使用继承…...
Vim 调用外部命令学习笔记
Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...
React hook之useRef
React useRef 详解 useRef 是 React 提供的一个 Hook,用于在函数组件中创建可变的引用对象。它在 React 开发中有多种重要用途,下面我将全面详细地介绍它的特性和用法。 基本概念 1. 创建 ref const refContainer useRef(initialValue);initialValu…...

.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
macOS多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用
文章目录 问题现象问题原因解决办法 问题现象 macOS启动台(Launchpad)多出来了:Google云端硬盘、YouTube、表格、幻灯片、Gmail、Google文档等应用。 问题原因 很明显,都是Google家的办公全家桶。这些应用并不是通过独立安装的…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...

Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...