本文源码均来源于 JDK 1.8
什么是 AQS
AQS 指的是 AbstractQueuedSynchronizer 这个类,
它提供了一个用于构建依赖于 FIFO 等待队列的阻塞锁和与之相关的同步器的框架,
比如 ReentrantLock、CountDownLatch 都是基于 AQS 的
来看一看 AbstractQueuedSynchronizer 的源码里大概都有些什么,这里只列举部分核心方法,其它核心方法会在后面详细介绍
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public class ConditionObject implements Condition, java.io.Serializable {}
static final class Node {}
// 同步状态,使用volatile修饰保证线程可见性
private volatile int state;
protected final int getState() {}
protected final void setState(int newState) {}
// 原子地(CAS)设置同步状态值
protected final boolean compareAndSetState(int expect, int update) {}
// 独占模式获取同步状态,不理解什么是独占模式也不要紧,后面会详细解释
public final void acquire(int arg) {}
// 独占模式释放同步状态
public final boolean release(int arg) {}
// 尝试使用独占模式获取同步状态
protected boolean tryAcquire(int arg) {}
// 尝试使用独占模式释放同步状态
protected boolean tryRelease(int arg) {}
}
可以看到,AbstractQueuedSynchronizer 拥有 2 个内部类:ConditionObject 和 Node。如果使用过 ReentrantLock,那么对于 lock.newCondition() 一定非常熟悉,newCondition 返回的就是 ConditionObject 实例,
但本文暂不对 ConditionObject 做深入探讨
AQS 原理
AQS 核心思想:
- 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态
- 如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中
CLH 队列
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系,即表现为双向链表)。
AQS 将每个请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node,也即前面看到的 Node 内部类)来实现锁的分配
下图是 CLH 队列示意图,注意 CLH 队列包含 head 节点,但阻塞队列不包含 head 节点,因为 head 节点是占有锁的线程所在节点,这一点在后面的源码分析也能体现
来看一下 Node 类的源码
static final class Node {
/** 默认为共享模式的节点 */
static final Node SHARED = new Node();
/** 独占模式 */
static final Node EXCLUSIVE = null;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
// 本文暂不分析此属性,此属性用于Condition
Node nextWaiter;
/** waitStatus value:线程已取消,不再争抢这个锁 */
static final int CANCELLED = 1;
/** waitStatus value:当前node的后继节点对应的线程需要被唤醒 */
static final int SIGNAL = -1;
/** waitStatus value:线程正在等待,表明还在队列中,用于 Condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
}
可以发现,在 CLH 队列中,每个 Node 保存了线程的引用、状态、前驱节点、后继节点,并且状态有 4 个取值分别对应不同的状态,如果大于 0,则代表这个线程取消了等待
同步状态的获取 - 抢锁
独占式
独占式,指同一时刻只能有一个线程持有同步状态(锁)
acquire(int arg)
此方法对中断不敏感,也就是说如果线程获取同步状态失败加入到 CLH 同步队列,后续对该线程进行中断操作时,线程并不会从同步队列中移除
public final void acquire(int arg) {
// 首先调用tryAcquire,试一下,如果成功就不需要进队列排队了
// 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就理所当然可以直接获取
// 对于公平锁的语义:本来就没人持有锁,根本没必要进队列等待
if (!tryAcquire(arg) &&
// tryAcquire(arg) 没有成功,这个时候需要把当前线程挂起,放到阻塞队列中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 将当前线程加入到 CLH 同步队列尾部
// 参数 mode 此时是 Node.EXCLUSIVE,代表独占模式
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;
// tail != null => CLH 队列不为空
if (pred != null) {
node.prev = pred;
// 如果成功,tail == node
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果执行到这里,说明 pred == null(队列是空的) 或者 CAS失败(有线程在竞争入队)
enq(node);
return node;
}
// 采用自旋的方式入队
// 自旋的语义:CAS 设置 tail 过程中,竞争一次竞争不到,就多次竞争,总会排到
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 细心的同学会发现,原来 head 和 tail 在 AbstractQueuedSynchronizer 初始化的时候都是 null
if (t == null) { // Must initialize
// CAS 初始化 head,可能有很多线程同时进来
// 如果对 Node 的源码还有印象,可以知道现在 head 没有线程引用、前驱节点和后继节点
// 注意:这个时候 head 节点的 waitStatus == 0,Node() 构造方法为空实现
if (compareAndSetHead(new Node()))
// 注意这里没有 return,只是初始化了 head 节点,并将 tail 指向 head
tail = head;
} else {
// 如果 head 节点在上一轮循环才初始化,那么 node.prev = head
node.prev = t;
// 将当前线程排到队尾,有线程竞争的话排不上重复排
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 参数 node,经过 addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列
// 如果此方法返回 true 的话,意味着 acquire 方法将进入 selfInterrupt 方法
// static void selfInterrupt() {
// Thread.currentThread().interrupt();
// }
// 所以正常情况下应该返回 false
// 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// predecessor() 返回 node 的上一个节点
final Node p = node.predecessor();
// p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个
// 注意,阻塞队列不包含 head 节点,head 一般指的是占有锁的线程,head 后面的才称为阻塞队列
// 所以当前节点可以去试抢一下锁,这里说一下为什么可以去试试:
// 1.它是队头
// 2.当前的 head 有可能是刚刚初始化的 node
// enq(node) 方法里面有提到,head 是延时初始化的,而且 new Node() 的时候没有设置任何线程
// 也就是说,当前的 head 不属于任何一个线程,所以作为队头,可以去试一试
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 执行到这里,说明上面的 if 分支没有成功,没有抢到锁
// 要么当前 node 不是队头,要么就是 tryAcquire(arg) 没有抢赢别人
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// tryAcquire() 方法抛异常的情况下,failed == true
// 取消抢锁操作
if (failed)
cancelAcquire(node);
}
}
// 没有抢到锁时才会执行此方法,此方法的意思是:当前线程没有抢到锁,是否需要挂起当前线程
// 第一个参数是前驱节点,第二个参数是当前线程的节点
private static boolean shouldParkAfterFailedAcquire(AbstractQueuedSynchronizer.Node pred, AbstractQueuedSynchronizer.Node node) {
int ws = pred.waitStatus;
// 前驱节点的 waitStatus == -1,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
if (ws == AbstractQueuedSynchronizer.Node.SIGNAL)
return true;
// 前驱节点 waitStatus大于 0 ,之前说过,大于 0 说明前驱节点取消了排队
// 这里需要知道:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的
// 所以下面这块代码说的是将当前节点的 prev 指向 waitStatus<=0 的节点
// 因为当前线程得依赖前驱节点来唤醒呢
// 到这里可以发现 Node 的 waitStatus 属性其实是对于后继节点有效的
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 执行到这里表示 waitStatus 不等于 -1 和 1,那也就是只可能是 0、-2、-3
// 在我们前面的源码中,都没有看到有设置 waitStatus 的,所以每个新的 node 入队时,waitStatu 都是0
// 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0
// 用 CAS 将前驱节点的 waitStatus 设为 Node.SIGNAL(也就是-1)
compareAndSetWaitStatus(pred, ws, AbstractQueuedSynchronizer.Node.SIGNAL);
}
// 这里返回 false,在 acquireQueued 方法中会进行下一轮 for 循环
// 直到将前驱节点的 waitStatus 置为 -1,再下一轮就会通过第一个 if 分支返回 true
return false;
}
// 如果 shouldParkAfterFailedAcquire(p, node) 返回 true,
// 那么需要执行 parkAndCheckInterrupt()
// 因为前面返回 true,所以需要挂起线程,这个方法就是负责挂起线程的
// 这里用了 LockSupport.park(this) 来挂起线程,然后就停在这里了,等待被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
acquireInterruptibly(int arg)
此方法在等待获取同步状态时,如果当前线程被中断了,会立刻响应中断抛出异常 InterruptedException
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
doAcquireInterruptibly(int arg) 与 acquire(int arg) 仅有两个差别:
- 方法声明抛出 InterruptedException 异常
- 在中断方法处不再是使用 interrupted 标志,而是直接抛出 InterruptedException 异常
tryAcquireNanos(int arg, long nanosTimeout)
此方法为 acquireInterruptibly 方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回 false,否则返回 true
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
针对超时控制,程序首先记录唤醒时间 deadline,deadline = System.nanoTime() + nanosTimeout。如果获取同步状态失败,则需要计算出需要休眠的时间间隔 nanosTimeout(= deadline - System.nanoTime()),如果 nanosTimeout <= 0 表示已经超时了,返回 false,如果大于 spinForTimeoutThreshold(1000L) 则需要休眠 nanosTimeout ,如果 nanosTimeout <= spinForTimeoutThreshold ,就不需要休眠了,直接进入快速自旋的过程。原因在于 spinForTimeoutThreshold 已经非常小了,非常短的时间等待无法做到十分精确,如果这时再次进行超时等待,相反会让 nanosTimeout 的超时从整体上面表现得不是那么精确,所以在超时非常短的场景中,AQS 会进行无条件的快速自旋
共享式
共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞
acquireShared(int arg)
此方法对中断不敏感,也就是说如果线程获取同步状态失败加入到 CLH 同步队列,后续对该线程进行中断操作时,线程并不会从同步队列中移除
public final void acquireShared(int arg) {
// tryAcquireShared(arg) < 0 代表失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
// r >= 0 表示获取到同步状态
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
AQS 也提供了共享式响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg, long nanosTimeout),这里就不做解释了
同步状态的释放 - 解锁
独占式
独占式同步状态释放使用 release(int arg) 方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 参数 node 是 head 头结点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus == 1)
// 从队尾往前找,找到 waitStatus<=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);
}
共享式
共享式同步状态释放使用 releaseShared(int arg) 方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// Node.SIGNAL == -1
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
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);
}
总结
在并发环境下,加锁和解锁需要以下三个部件的协调:
- 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state 进行 +1 就可以,解锁就是 -1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对。然后唤醒等待队列中的第一个线程,让其来占有锁
- 线程的阻塞和解除阻塞。AQS 中采用了
LockSupport.park(thread)来挂起线程,用LockSupport.unpark(thread)来唤醒线程 - 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用
参考: