1.AQS概述
AQS(AbstractQueuedSynchronizer)是JUC并发包中的一个基类,用于构建锁和其他同步组件,如 ReentrantLock、Semaphore、CyclicBarrier、CountDownLatch、ThreadPoolExecutor等等都是基于AQS实现。
接下来本篇文章将以 ReentrantLock 做分析AQS的加锁、释放锁、取消锁来的过程。
2.为什么需要AQS
传统同步机制 synchronized 使用简单直接,其底层原理是编译器编译后会在同步块前后生成 monitorenter 和 monitorexit 字节码指令,靠对象头里的标记来控制锁的获取和释放。这样的同步机制功能单一,拓展性差,不够灵活。
AQS 则解决了同步器设计的通用性问题,AQS 将同步状态、线程排队、等待/唤醒等通用逻辑抽象出来,开发者能够根据业务情况自定义或拓展所需的同步规则,实现更精细化的并发控制。
3.核心属性
在AQS中,其内部关键要素主要有以两大核心:
- state:一个由 volatile 修饰,并且基于CAS修改的 int 类型的成员变量,
- node双向队列:通过Node对象串联起来的双向链表,当线程竞争state 资源失败时,会把自己封装为 Node 对象放到双向队列中排队。Node 是AQS中的内部类,其中主要的成员变量如下:
static final class Node { // 共享锁,表示当前创建的节点是共享锁 static final Node SHARED = new Node(); // 排他锁,表示当前创建的节点是排他锁 static final Node EXCLUSIVE = null; // waitStatus的值,表示当前节点取消状态 static final int CANCELLED = 1; // 表示队列后续节点待唤醒 static final int SIGNAL = -1; // waitStatus的值,表示该结点(对应的线程)在等待某一条件 static final int CONDITION = -2; // waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点) static final int PROPAGATE = -3; // 描述当前节点状态,取值:-1,-2,-3,0,默认值为 0 volatile int waitStatus; // 当前节点的前一个节点 volatile Node prev; // 当前节点的后继节点 volatile Node next; // 当前节点对应的线程 volatile Thread thread; }
4.从源码看AQS的工作原理
下面将从 ReentrantLock 入手分析AQS加锁和释放锁的流程
4.1.加锁流程分析
以公平所为例
4.1.1.概述
如上图,假设有A、B、C、D 4个线程竞争锁;
- 线程A先基于CAS将 state 从0 修改为 1,线程A就获取到锁资源,可以执行业务代码
- 线程B、C、D再想通过CAS修改 state 状态,发现已经是 1,无法获取锁资源
- 线程B、C、D依次将自己封装为Node对象,加入到双向队列中排队。
3.1. 如果队列为空,则新建一个伪节点作为头节点(伪节点 thread = null)加入到双向队列中
3.2. 如果队列不为空,将B、C、D线程对应的Node挂到tail节点后面,修改上一个节点waitStatus=-1,把 tail 指向队列最后一个node节点
4.1.2. lock 源码分析
4.1.2.1. lock方法
- 执行lock方法后,公平锁和非公平锁执行逻辑有所不同
// 非公平锁
final void lock() {
// 上来就基于CAS的方式,尝试将state从0改为1
if (compareAndSetState(0, 1))
// 获取锁成功,设置 exclusiveOwnerThread 为当前线程,表示当前线程持有锁资源,后续判断锁重入也用到
setExclusiveOwnerThread(Thread.currentThread());
else
// 上锁失败
acquire(1);
}
// 公平锁
final void lock() {
acquire(1);
}
- acquire 方法(AQS的方法),从上面的代码看出,不管公平锁还是非公平锁,在获取锁的过程中,都
有可能
执行 acquire ,非公平锁在一开始上锁失败才会执行 acquire 方法,所以 公平锁和非公平锁的 acquire 的逻辑都一样
public final void acquire(int arg) {
// 当前线程尝试获取锁
if (!tryAcquire(arg) &&
// 获取锁失败
// addWaiter(Node.EXCLUSIVE):把当前包装成 Node 节点,加入到双休队列尾部
// acquireQueued:判断新加入节点是否是第一个排队,如果是,则尝试再获取锁,获取不到则挂起线程; 如果不是第一个排队的节点,则直接挂起线程
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 中断线程
selfInterrupt();
}
- 非公平锁 tryAcquire 的实现
// 非公平锁的实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
// 判断当前 state 是否为0
if (c == 0) {
// 再抢一次锁
if (compareAndSetState(0, acquires)) {
// 抢锁成功,设置当前线程为获得锁资源线程
setExclusiveOwnerThread(current);
return true;
}
}
// state 不为0,判断当前取得锁的线程是否等于当前线程
else if (current == getExclusiveOwnerThread()) {
// 如果是,锁重入,state + 1
int nextc = c + acquires;
if (nextc < 0) // overflow
// 锁重入不能超过 int 数值最大值。
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- 公平锁 tryAcquire 的实现
// 公平锁 tryAcquire 的实现
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 判断队列中是否有节点在排队,如果没有在排队的,抢一下锁资源。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 抢锁成功,设置当前线程为获取锁资源的线程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 当前线程 == 获取锁资源的线程,锁重入,state + 1,
// state 累加到 int 值极限时,会变为负值,所以 state 的最大值就是 int 的最大值。
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 查看是否有线程在AQS的双向队列中排队
// 返回false,代表没人排队
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源
return h != t &&
// s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)
((s = h.next) == null || s.thread != Thread.currentThread());
}
- acquireQueued方法,判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者又没获取到,尝试将当前线程挂起
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)) {
// 如果上锁成功,把当前获取锁成功的node设置到头节点,
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 没获取到锁资源
// shouldParkAfterFailedAcquire:基于上一个节点状态来判断当前节点是否能够挂起线程,如果可以返回true,如果不能,就返回false,继续下次循环
if (shouldParkAfterFailedAcquire(p, node) &&
// parkAndCheckInterrupt 基于Unsafe类的park方法,将当前线程挂起
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 获取锁资源成功后,先执行setHead
private void setHead(Node node) {
// 当前节点作为头结点 伪
head = node;
// 头结点不需要线程信息
node.thread = null;
node.prev = null;
}
// 当前Node没拿到锁资源,判断是否能挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点
// 1,CANCELLED状态:代表当前节点已经取消
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 上一个节点为-1之后,当前节点才可以安心的挂起线程
return true;
if (ws > 0) {
// 如果当前节点的上一个节点是取消状态,需要遍历往前找到一个状态不为1的Node,作为当前他的next节点
// 找到状态不为1的节点后,设置一下next和prev
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
4.1.2.2. tryLock方法
对于公平锁和非公平锁,最终都会走非公平锁抢占锁资源的操作,拿到state,如果是0,尝试CAS抢一下,不是0,那就判断是否重入锁。如果还没抢到,就返回false,抢锁失败
// tryLock方法,无论公平锁还有非公平锁。都会走非公平锁抢占锁资源的操作
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 拿到state的值, 如果是0,直接CAS浅尝一下
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//state 不是0,那就看下是不是锁重入操作
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果没抢到,或者不是锁重入操作,告辞,返回false
return false;
}
tryLock(time,unit) 分析
具体步骤分为:
- 判断现场是否被中断,如果中断直接抛异常;
- 尝试通过 tryAcquire 获取锁,如果获取锁失败,则等待指定时间
2.1. 设置结束时间,把节点放到双向队列中
2.2. for死循环从双向队列,向前遍历获取node 节点,如果当前节点的上一个节点为 head 节点,直接抢资源。
3.3. 如果没有抢到锁,剩余足够的时间,挂起线程剩余时间。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// tryLock(time,unit)执行的方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {
// 线程的中断标记位,是不是从false,被改为了true,如果是,直接抛异常
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire分为公平和非公平锁两种执行方式,如果拿锁成功, 直接告辞,
return tryAcquire(arg) ||
// 如果拿锁失败,在这要等待指定时间
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待时间是0秒,直接告辞,拿锁失败
if (nanosTimeout <= 0L)
return false;
// 设置结束时间。
final long deadline = System.nanoTime() + nanosTimeout;
// 先扔到AQS队列
final Node node = addWaiter(Node.EXCLUSIVE);
// 拿锁失败,默认true
boolean failed = true;
try {
for (;;) {
// 如果在AQS中,当前node是head的next,直接抢锁
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;
// shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 避免剩余时间太少,如果剩余时间少就不用挂起线程
nanosTimeout > spinForTimeoutThreshold)
// 如果剩余时间足够,将线程挂起剩余时间
LockSupport.parkNanos(this, nanosTimeout);
// 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
if (Thread.interrupted())
// 是中断唤醒的!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4.1.2.3. lockInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
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);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
4.2.释放锁流程分析
4.2.1.概述
如上图,假设线程A获取到锁资源,重入了一次,state=2
线程 B、C、D 获取锁资源失败,在AQS中排队
1、线程A释放调用 unlock 锁资源,执行 tryRelease 方法
2、判断是否是线程A持有锁资源,如果不是,抛出异常
3、如果是线程A,对 state-1,如果 state扣减1后,不为0,结束方法
4、如果state-1为0,则资源释放干净,查看头节点的状态是否不为0,如果为0,说明后面没有挂起的线程,如不为0,后续链表中有挂起的线程,需要唤醒。
5、在唤醒线程时,需要将当前的-1改为0,找到有效节点唤醒。
4.2.2.unlock源码分析
public void unlock() {
// 释放锁资源不分为公平锁和非公平锁,都执行同一个方法
sync.release(1);
}
// 释放锁流程
public final boolean release(int arg) {
// 核心释放锁资源的操作之一
if (tryRelease(arg)) {
// 如果锁已经释放掉了,走这个逻辑
Node h = head;
// h不为null并且状态不为0(为-1),后面有排队的Node,线程处于挂起状态
if (h != null && h.waitStatus != 0)
// 唤醒排队的线程
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock 释放锁资源操作
protected final boolean tryRelease(int releases) {
// 拿到state - 1(并没有赋值给state)
int c = getState() - releases;
// 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free,代表当前锁资源是否释放干净了。
boolean free = false;
if (c == 0) {
// 如果state - 1后的值为0,代表释放干净了。
free = true;
// 将持有锁的线程设置null
setExclusiveOwnerThread(null);
}
// 将c设置给state
setState(c);
// 锁资源释放干净true,否则false
return free;
}
// 唤醒排队的节点
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
if (ws < 0)
// 先基于CAS,将节点状态从-1,改为0
compareAndSetWaitStatus(node, ws, 0);
// 拿到头节点的后续节点。
Node s = node.next;
// 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
// 从后往前找到状态小于等于0的节点
// 找到离head最新的有效节点,并赋值给s
if (t.waitStatus <= 0)
s = t;
}
// 只要找到了这个需要被唤醒的节点,执行unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
4.3.取消节点流程分析
4.3.1概述
如上图,假设双向队列中线程C,不需要执行任务了,执行了cancelAcquire 方法
1、线程C对应节点 thread 设置为 null
2、往前找到有效节点作为当前节点的 prev
3、将 waitStatus 设置为 1,代表取消
4、脱离 AQS 队列
4.3.2 cancelAcquire流程分析
// 取消在AQS中排队的Node
private void cancelAcquire(Node node) {
// 如果当前节点为null,直接忽略。
if (node == null)
return;
//1. 线程设置为null
node.thread = null;
//2. 往前跳过被取消的节点,找到一个有效节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//3. 拿到了上一个节点之前的next
Node predNext = pred.next;
//4. 当前节点状态设置为1,代表节点取消
node.waitStatus = Node.CANCELLED;
// 脱离AQS队列的操作
// 当前Node是尾结点,将tail从当前节点替换为上一个节点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// 到这,上面的操作CAS操作失败
int ws = pred.waitStatus;
// 不是head的后继节点
if (pred != head &&
// 拿到上一个节点的状态,只要上一个节点的状态不是取消状态,就改为-1
(ws == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
&& pred.thread != null) {
// 上面的判断都是为了避免后面节点无法被唤醒。
// 前继节点是有效节点,可以唤醒后面的节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 当前节点是head的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
5.应用场景
AQS的核心价值是提供了模板方法,将同步状态的原子性管理与线程解耦,开发者只需关注状态逻辑的装换即可实现高效的同步器。
其实现的同步器有:
- ReentrantLock(需要重入互斥时,可用于多线程同时修改共享资源)
- Semaphore(限制并发线程数、多线程任务等待统一事件触发)
- CountDownLatch(限制并发线程数、多线程任务等待统一事件触发)
- CyclicBarrier
- ReentrantReadWriteLock (读多写少的场景)
- ConditionObject
- 可自定义同步器
6.常见问题
为什么AQS选择使用双向队列,而不是单向队列?
AQS中一般是存放没有获取到资源的Node,在竞争锁资源时,ReentrantLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置位1,并且从AQS队列中移除掉。如果采用单向链表,当前节点只能按到后继或者前继节点,这样是无法将前继节点指向后继节点的,需要遍历整个AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。
当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多次无效的遍历操作,如果是双向链表就可以解决这个问题。
7.总结
AQS 凭借其出色的模板方法,可以衍生出很多方便且使用的JUC 工具,并且易于使用,方便拓展。