并发之AQS的双向链表
谈谈 AQS
AQS(AbstractQueuedSynchronizer)
是JUC
包下的一个抽象类。虽然是抽象类,但没有抽象方法,即便子类集成,也无法直接使用锁功能。AQS中关于锁的判断TryAcquire
与TryRelease
方法,默认都是报错,需要子类集成后进行重写,才能使用锁功能。
JUC
包的一些并发功能都是基于AQS
实现的,例如:ReentrantLock
、ThreadPoolExecutor
、并发集合等。
AQS的实现包含:一属性,两队列。
一属性:state
。表示锁的状态。初始化为0
表示无锁,获取锁后+1
,释放锁时-1
。一般在TryAcquire
与TryRelease
中实现+、-
功能。
两队列:双向队列与单向队列。
双向队列
双向链表,也称作等待队列、阻塞队列。是在线程获取锁失败后的等待处理操作。AQS中存在<Node> head
、<Node> tail
属性用于组成双向队列。
重点关注的几个重点词:
-
AbstractQueuedSynchronizer存在
head
与tail
属性,所以其本身就是一个链表。并没有使用集合 -
双向链表(等待队列)
head
永远都是伪节点(thead = null)tail
初始化时是伪节点(初始化时, head == tail),之后就不是了。
-
node的作用就是封装线程信息,然后并放到链表中排队
-
node节点有5种状态:
- 用于双向链表(CANCELLED、SIGNAL、0)
- 用于单向链表(CONDITION、CANCELLED)
- 用于共享锁(PROPAGATE)
-
双向链表中节点状态
tail
的节点状态永远是 0head
初始化为0,之后变为-1- 中间节点的状态为 -1
- 被取消(无效/中断)的节点状态为 1。
-
挂起线程:
LockSupport.pack(thread)
-
唤醒线程:
LockSupport.unpack(thread)
-
获取锁操作:
acquire -> tryAcquire -> addWaiter -> acquireQueued(死循环)
-> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt -> LockSupport.park
-> setHead(可以认为,删除唤醒节点)
-
获取锁异常操作:
cancelAcquire
-
取消锁操作:
release -> tryRelease -> unparkSuccessor -> LockSupport.unpark
-
双向链表中是否可以不用head节点?
可以不用。- 在设计之前就提出了伪节点的存在,
- head节点的使用可以简化
-
为什么是双向链表,而不是用单线链表?
因为在使用单线链表时,删除中间节点时,无法将node.prev.next 指向node.next。解决方法只能不断遍历,增加了很多无用操作。
而使用双向链表就没有这个问题
锁的获取与释放 - 获取锁代码步骤
1. acquire 开启锁入口
所有调用锁功能的入口
2. tryAcquire 尝试获取锁
尝试获取锁。需要子类方法去重写此类。返回true
,表示获取到锁,返回false
。表示未获取到锁,代码才会往后走。
大体逻辑:
- CAS,如果
AQS.state = 0
,则将AQS.state = 1
- 修改
aqs.thread = 当前线程
AQS
中对于TryAcquire
需要子类重新,这里,我们使用ThreadPoolExecutor.Worker
中TryAcquire
方法(主要是代码简单)
3. addWaiter 将线程放入等待队列中
没有获取到锁,创建一个node节点,并放入双向队列的中,队列遵循FIFO。enq()是head与tail初始化方法。可以细看一下,代码简单,在这里不做描述。
4. acquireQueued(死循环) 将刚加入线程,挂起
方法体中存在死循环,线程被挂起后,基本上两圈半后,就会返回 true,结束循环。
- 第一圈,将前置节点状态改为 -1。
- 第二圈,将线程挂起。
- 半圈,线程被唤醒后成为head,变为伪节点。
而被唤醒的线程一定是Head.next节点。当然如果,线程本身就是head.next,则直接过去到锁。不会挂起。
5. shouldParkAfterFailedAcquire 挂起前准备,将前置节点状态改为 -1
将 node.prev节点状态改为 -1,并返回false。当返回true后,才会执行 parkAndCheckInterrupt方法,挂起。
6. parkAndCheckInterrupt 挂起线程(LockSupport.park)
挂起线程,查看线程的唤醒,是因为异常报错,还是正常唤醒。
7. setHead 被唤醒的节点,变为head(可以认为,删除唤醒节点)
能走到setHead方法,说明已经获取锁。并将node代替head节点。
需要注意的事:head.next 不能设置为空。释放锁时用。
锁的获取与释放 - 释放锁代码步骤
1. release 释放锁入口
释放锁的两个动作:1.将AQS.state--,2.唤醒 head.next线程
2. tryRelease 尝试释放锁
与TryAcquire
相同,需要子类重写此方法。
大体逻辑:
- 判断 AQS.state == 0。只有等于0,才说明完全释放锁,才会有后面动作。大于0,说明锁未完全释放。
- 设置 AQS.thread =null
为了能体现,AQS.state > 0
的情况,我们查看ReentrantLock
的TryRelease
方法
3. unparkSuccessor 唤醒下一个节点 LockSupport.unpark(head.next.thread);
唤醒 head.next
线程。被唤醒的节点,会继续执行acquireQueued
的方法。
假如head.next.waitStatus =1
,也就是说,下一个节点被取消了,不能唤醒。则从tail
上前寻找距离head
最近的有效节点,并唤醒。
通过
acquireQueued方法,可以知道被唤醒的节点最终会成为
head。
锁的获取与释放 - 获取锁异常代码步骤
1. cancelAcquire 取消在AQS(双向链表)中的node
代码一般被写到finally块内,保证获取锁异常时一定执行
取消节点的操作流程:
- 将node.thread = null
- 往前找到有的节点作为node.prev
- 将 node.waitStatue = 1,表示节点取消
- 将node 脱离 双向链表,分三种情况
4.1 当node 是tail,直接删除
4.2 当node 是 head.next,删除 并唤醒下一个节点
4.3 当node 是中间节点,删除 并确保node.prev.waitStatus一定为-1
__EOF__

本文链接:https://www.cnblogs.com/zz-1q/p/17840513.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)