JUC 底层核心支持 AQS 源码分析
属性
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 头节点
private transient volatile Node head;
// 尾节点
private transient volatile Node tail;
// 资源状态(0:初始状态;1:已经被持有,大于1:多个线程重复持有,也就是重入了)
private volatile int state;
}
内部类
static final class Node {
// 占用资源方式:共享
static final Node SHARED = new Node();
// 占用资源方式:独占
static final Node EXCLUSIVE = null;
// 当前节点的线程被取消了,这个节点的线程不会再拿到锁
static final int CANCELLED = 1;
// 线程已经准备好了,就等资源释放
static final int SIGNAL = -1;
// 表示节点在等待队列中,等待被唤醒
static final int CONDITION = -2;
// 共享锁模式下才会使用,独占锁不会用
static final int PROPAGATE = -3;
// 当前节点状态,取值就是上面的 4 个[1,-1,-2,-3]
volatile int waitStatus;
// 上一个节点
volatile Node prev;
// 下一个节点
volatile Node next;
// 节点持有的线程(每个节点都会包装成一个节点放入队列)
volatile Thread thread;
// 也是下一个节点,指向下一个处于 CONDITION 状态的节点
Node nextWaiter;
}
核心方法
- tryAcquire(arg):获取锁,如果获取锁失败,继续判断别的条件。是一个模板方法,具体怎么获取由子类实现(公平、非公平、读写锁)
- addWaiter(Node.EXCLUSIVE):当前线程包装成 node,并进入队列
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):修改上一个节点状态,并把当前节点的线程挂起
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
获取锁
tryAcquire 这个方法 AQS 只提供了一个模板,具体要由子类实现
线程入队
分为两个步骤,一个是把线程封装成 node,并维护好链表关系,一个是进入队列
// 参数是模式,模式为:Node.EXCLUSIVE(独占), Node.SHARED(共享)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 封装线程为 Node
Node pred = tail; // 尾节点
// 如果尾节点不为空,说明队列已经有节点了,当前队列需要维护链表关系,维护好就完了
if (pred != null) {
node.prev = pred; // 原来尾节点设置为当前节点的上一个节点(维护链表,这里只是单向)
if (compareAndSetTail(pred, node)) { // 设置当前节点为新的尾节点
pred.next = node; // 原来尾节点的下一个节点设置为当前节点(这里维护后就是双向链表了)
return node; // 返回当前封装后的 node
}
}
enq(node); // 入队,其实是初始化队列,此时队列还是空的(进入方法前 node 还没维护链表)
return node; // 返回封装后的 node(进入方法后,返回的 node 就已经维护好链表了)
}
// 入队,参数 node 是将要入队的节点,当队列不为空才会进这个队列
private Node enq(final Node node) {
for (;;) {
Node t = tail; // 尾节点
if (t == null) { // 此时尾节点是空
if (compareAndSetHead(new Node())) // 设置头节点,注意都节点是 new 出来的,并不是传进来的节点
tail = head; // 上面的 if 设置了头节点,head 就有值了,再把[头节点]赋值给[尾节点]
} else { // 因为是死循环,第一次循环设置了头尾节点,第二次循环尾节点不为空,就会走到这里了
node.prev = t; // 当前节点的上一个节点设置为原来的尾节点(这里只维护了单向链表)
if (compareAndSetTail(t, node)) { // 再设置当前节点为尾节点
t.next = node; // 原来尾节点的下一个节点设置为当前节点(这里维护了就成为了双向链表)
return t; // 返回原来的尾节点。跳出循环
}
}
}
}
节点入队后
- 入队后的节点主要处理两件事:更改上一节点的状态、挂起当前节点的线程
- 入队的节点可能是第二个节点,也可能不是第二个节点(第一个节点是虚节点)
- 如果是第二个节点,下一个就应该是它获取到锁,入队前尝试一下获取锁,能获取到就不用挂起了,但是有可能是非公平锁,所以不一定能获取到锁
- 如果不是第二个节点,没什么好说的直接挂起
/**
* 参数 node 是刚入队的节点
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // node.predecessor() 就是拿当前节点的上一个节点
if (p == head && tryAcquire(arg)) { // 如果 p == head(当前节点是第二个节点),执行 tryAcquire(arg) 方法尝试获取锁
setHead(node); // 如果抢到锁了,把当前节点(也就是第二个节点)设置为头节点
p.next = null; // 当前节点是第二个节点,说明当前节点就是队尾了,所以当前节点的尾节点置为空,help GC
failed = false;
return interrupted; // 返回 false(返回到 acquire(),继续执行 selfInterrupt())
}
// 如果当前节点不是第二个节点,或者没抢到锁,就要处理刚进来的节点,当前节点的线程要挂起
if (shouldParkAfterFailedAcquire(p, node) && // 因为这里是个死循环,这个方法一定会返回 true
// 底层用了 LockSupport.part(this) 挂起当前线程,唤醒后继续执行
parkAndCheckInterrupt())
interrupted = true;
}
} finally { // 如果异常
if (failed) // 异常的话 failed 就是 true
cancelAcquire(node); // 取消当前节点
}
}
// p 是上一个节点,node 是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 上一个节点可能是虚节点也可能不是虚节点,如果是虚节点,waitStatus 肯定是 0,如果不是虚节点就会是[1,-2,-3,-1]中的一个
int ws = pred.waitStatus; // 是上一个节点的状态
if (ws == Node.SIGNAL) // -1
return true;
if (ws > 0) { // 1
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // -2,-3,0
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 把上一个节点的状态改为 -1
}
return false; // 返回 false,外层的循环继续下一次循环,下一次循环的时候上一个节点的值是1,就会返回 true 了
}
释放锁
AQS 维护了链表和唤醒下一个节点,具体怎么释放锁由子类实现
// 释放锁,把 state 的值设置为 state - arg
public final boolean release(int arg) {
if (tryRelease(arg)) { // tryRelease() 是个模板方法,具体由子类实现,目的是把 state 的值 -1
Node h = head; // 头节点赋值给 h
if (h != null && h.waitStatus != 0) // 第二个节点入队把头节点的 waitStatus 改成了 -1,所以这个条件会是真,继续往下走
unparkSuccessor(h); // 唤醒队列中下一个线程
return true;
}
return false;
}
唤醒下一个节点
// 唤醒线程,node 是头节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus; // 头节点的 waitStatus 是 -1
if (ws < 0) // -1 < 0,进入
compareAndSetWaitStatus(node, ws, 0); // 把头节点的 waitStatue 设置为 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); // 唤醒下一个节点的线程
}
唤醒节点后的操作
节点挂起在这个方法中,唤醒后继续执行
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 唤醒后再次走这个循环,因为唤醒的是头节点的下一个节点,所以当前节点的上一个节点也就是头节点,就是维护链表,把当前被唤醒的节点维护成头节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 挂在这里的
interrupted = true; // 继续执行,因为是死循环,所以再次走一遍循环
}
} finally {
if (failed)
cancelAcquire(node);
}
}
判断当前队列是否有节点
这个方法主要体现锁的公平和非公平,如果是非公平锁,不会调用这个方法,直接尝试获取锁,如果是公平锁,调用这个方法,如果当前队列有节点就入队
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
分类:
JAVA - JUC
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具