接上一篇《Java并发系列(6)——AQS与显式锁的使用》
文章目录
5.4 AQS 实现原理
5.4.1 原理图
结合上图,先概述 AQS 的实现原理。
这里主要涉及三个类:
- AbstractQueuedSynchronizer;
- Node,AQS 的静态内部类;
- ConditionObject,AQS 的非静态内部类,非静态很关键,这意味着 ConditionObject 实例一创建出来,天生就会持有一个 AQS(实现类) 实例的引用。所以 ConditionObject 并没有一个 set 方法用来设置 AQS 或者 Lock 引用,但在很多应用场景,比如 ReentrantLock,ReentrantReadWriteLock 中,Condition 却能准确地和相应的 Lock 实例关联在一起。
5.4.1.1 数据结构
从整体来看:
-
AQS 上面挂着一个一个的 Node,形成双向链表队列。AQS 的 head 引用指向头节点,tail 引用指向尾节点,state 变量是用来记录锁状态的。
-
ConditionObject(实现了 Condition 接口) 上面也挂着一个一个的 Node,不过这里是单向链表队列。
-
Node 则表示在等待的线程。
-
Node 如果挂在 AQS 的队列上,则表示正在排队,等前面的节点释放锁就有机会获取锁。
-
Node 如果挂在 ConditionObject 的队列上,则表示正在某个 Condition 上 await。
Node 类共有 5 个成员变量:
- thread:线程对象;
- pre:前一个 Node 节点;
- next:后一个 Node 节点;
- waitStatus:节点状态,共 5 个可能的取值,分别代表 5 种不同的状态:
- SIGNAL(-1):表示后一个节点需要被唤醒,AQS 队列上的节点正常情况都是这个状态;
- CONDITION(-2):ConditionObject 队列上的节点正常情况都是这个状态;
- CANCELLED(1):获取锁失败(被打断,超时),或释放锁失败;
- PROPAGATE(-3):比较少见,仅当头结点释放共享锁并且下一个节点不需要被唤醒时会出现此状态;
- 0:初始态,或中间态,很快就会切换到上面几种状态。
- nextWaiter:有两个不同的作用:
- 当节点在 AQS 队列时,nextWaiter 置为 null 表示当前节点的线程想要独占锁,否则为共享锁;
- 当节点在 ConditionObject 队列时,nextWaiter 指向下一个节点,同时当前线程一定是想要独占锁,因为必须持有独占锁才能使用 Condition。
5.4.1.2 AQS 如何管理线程
AQS 管理线程的动作:
- AQS 队列 head 节点:往往是一个空节点,thread == null,要么是刚开始初始化出来的,要么是正在执行的线程节点,要么是执行完了暂时还挂在队列上的;
- 抢到锁的线程:拿到锁直接执行自己逻辑去了,不会创建 Node 挂在 AQS 上;
- 未抢到锁的线程:AQS 新建一个 Node,thread 引用指向线程对象,接在 AQS 队列尾节点后面,成为新的尾节点;
- 释放锁:正在执行的线程释放锁,AQS 队列上 head 节点的下一个节点将被唤醒;
- 获得锁:一般情况,被唤醒的节点会获得锁,它会将 AQS 的 head 引用指向自己,将自己的 pre 引用置空,那么旧的 head 节点就从 AQS 队列上掉出去了;
- Condition#await:新建一个 Node,接在 ConditionObject 队列尾节点后面,成为新的尾节点,由于 await 会释放锁,因此 AQS 队列上 head 节点的下一个节点将会得到锁,自动将旧的 head 节点踢出去,于是就相当于把 AQS 队列头部搬到了 ConditionObject 队列尾部(这里两个 Node 不是同一个实例);
- Condition#signal:将 ConditionObject 的 firstWaiter 引用指向 head 节点的下一个节点,把原来的firstWaiter 节点从 ConditionObject 队列的头部搬到 AQS 队列尾部(这里的两个 Node 是同一个实例);
- 特殊情况:
- 共享锁:如果共享锁被获得,head 节点会试图将共享锁沿着队列传播下去,即,如果下一个节点也是要共享锁的,那么把下一个节点也唤醒(下一个节点被唤醒获得共享锁后会成为新的 head 节点,与前面获得锁的情况相同),如果下下个节点也是要共享锁的,那么把下下个节点也唤醒(又成为新的 head 节点),依此类推,直到遇到一个想要独占锁的节点为止;
- 插队:AQS 允许新来的线程直接尝试获取一次锁,如果能拿到锁,就不会创建 Node 到 AQS 队列末尾排队。
5.4.1.3 AQS 是公平锁还是非公平锁
这个说法是一个坑,AQS 本身不是锁,它负责管理线程,所以 AQS 没有是公平锁还是非公平锁的说法。
但是,根据 AQS 的实现原理,个人认为:
- 它带有一定的非公平性:因为 AQS 允许插队,在实现锁的时候如果不做特殊处理,就会成为非公平锁;
- 它也带有一定的公平性:因为在没有线程插队拿锁的情况下,AQS 队列上的线程是按顺序依次唤醒拿锁的。
5.4.2 核心源码
AQS 类方法很多,这里只分析一些能反映 AQS 实现原理的核心方法。AQS 实际上是一个模板,核心方法都用了模板模式。
5.4.2.1 acquire 方法
获取锁的系列方法:
- acquire:获取独占锁;
- acquireInterruptibly:获取独占锁,支持 interrupt;
- tryAcquireNanos:获取独占锁,支持 interrupt,超时直接返回;
- acquireShared:获取共享锁;
- acquireSharedInterruptibly:获取共享锁,支持 interrupt;
- tryAcquireSharedNanos:获取共享锁,支持 interrupt,超时直接返回。
这里看一下 acquire 方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这是获取独占锁的方法模板,规定了获取锁共有以下几个步骤:
- tryAcquire:尝试抢一次锁;
- addWaiter:如果没抢到,就加入到 AQS 队列排队等锁;
- acquireQueued:在 AQS 队列上自旋,不断尝试获取锁,获取锁失败会阻塞(并非每次都会);
- selfInterrupt:如果不是被前面的节点唤醒,而是被打断醒来,这里重新置位打断状态。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
tryAcquire 方法决定线程是否能获得锁,没有实现,需要子类实现。如果能获得锁,子类实现应该返回 true,否则返回 false。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter 方法整体流程:
- 以独占模式新建 Node;
- 尾节点不为空,把 Node 直接接在尾节点后面;
- 尾节点为空,则 enq(node),也是把 Node 添加到 AQS 队列末尾,但还做了一些其它的事情;
- 返回新建的 Node。
再看一些细节:
Node node = new Node(Thread.currentThread(), mode);
节点里面塞入了当前线程对象。
mode 参数可以从前面找到传入的是 Node.EXCLUSIVE。
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
Node.EXCLUSIVE 可以从 Node 类源码找到实际上是 null,代表的是独占模式。
这里的构造方法:
Node(Thread thread, Node mode) {
// Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
mode 被传递给了 Node 类的 nextWaiter 成员变量,所以 Node 节点 nextWaiter 为 null 就表示这个线程是想要获取独占锁的。
再看 enq(node):
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
还是把 Node 接到尾节点后面,同时这个方法里面会做 AQS 队列的初始化操作。并且这里是一个 CAS 自旋,必须把 Node 加到队列末尾,直到成功为止。
这里可以看到 AQS 队列的头节点,它被初始化为一个 new Node() 空节点,不包含 Thread 信息,这是头节点的第一种情况。
到这里 addWaiter 方法就看完了。
再看 acquireQueued 方法,获取锁的主要逻辑都在这里:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
核心逻辑就是一个 for(;😉 自旋。
final Node p = node.predecessor();
拿到当前 Node 的前一个节点。
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
只有当 p == head,即前一个节点为头节点时,才 tryAcquire,又调用需要子类实现的“决定当前线程是否能获得锁”的方法。
如果 tryAcquire 也成功了,意味着当前线程拿到了独占锁,于是 setHead 把当前节点设置为头节点:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
可以看到头节点 thread 是被设为 null 的。这里是头节点的第二种情况:获得锁正在执行。
设置了新的头节点,指向原来的头节点的所有引用都断开,原来的头节点就从 AQS 队列上出去可以被 GC 了。
如果 p != head,后一半条件就不会走,不会尝试拿锁,直接跳过。前一个节点不是头节点,意味着当前节点前面还有节点在排队等锁,所以还轮不到当前节点拿锁。
p != head,当前节点处在队列中间位置,那么它为什么会从阻塞中醒来?interrupt 是其中一种情况,也可能会存在操作系统层面的意外唤醒情况。当然意外醒来也没有关系,接着看:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
这里又是 if(… && …) 的形式,JDK 的源码写得非常简洁,经常把方法调用写在 if 判断条件里面,然后利用逻辑运算的短路来控制方法调用,习惯就好。
实际上这里的逻辑是:如果 shouldParkAfterFailedAcquire 成立就执行 parkAndCheckInterrupt 方法,if 块里面的 interrupted = true 这句反而没那么重要。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire 方法里面涉及到复杂的 Node 状态转换,这里用到了 Node 类的 waitStatus 字段,int 类型,初始为 0。
这里主要工作,是一定要把前一个节点的 waitStatus 设为 SIGNAL,只有节点状态为 SIGNAL,才会唤醒下一个节点。所以如果不把前面一个节点的 waitStatus 设为 SIGNAL,前面一个节点就不会唤醒当前节点,那么当前节点就不能阻塞。所以,这里可以看到只有 ws == Node.SIGNAL 才会返回 true,也就是当前节点可以阻塞了。
if (ws > 0) ,waitStatus > 0 的唯一情况是 CANCELLED,意味着节点最终获取锁失败了,acquire 方法已经返回了,但是 Node 暂时还挂在 AQS 队列里面,所以这里顺便把 CANCELLED 的无效节点清理掉。
其它情况不论如何都要把前一个节点的状态置位 SIGNAL。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这个方法里面做了阻塞操作,执行了 park 方法,当前线程就会阻塞在这里,并且在被唤醒之前,park 方法不会返回。
Thread.interrupted() 方法返回当前线程的 interrupt 状态,并且将 interrupt 状态清空(还原成未被打断的状态)。
现在再回头看 acquireQueued 方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false