markdown学而不思则罔,思而不学则殆 ---------《论语・为政》
1.初识AQS
AQS(AbstractQueuedSynchronizer)是 Java 并发包中很多同步组件的基础,想必大家很熟悉CountDownLatch
(线程栏珊) ,ReentrantLock
(可重入锁),Semaphore
(信号量)这三个JUC包下常用的管理并发线程的组件,而AQS是它们内部类Sync的共同父类,本文就详细拆解AQS源码和工作原理,帮助我们深入理解并发实战。
2.AQS结构和原理
折叠Node类,AQS结构实际非常简单,state字段管理状态,而Node内部类管理并发线程,下面我们分开拆解这两者
2.1 state变量
先来看下官方注解,直接翻译是同步状态,并且由volatile
关键字修饰,多个线程可能存在读取不一致情况
arduino
/**
* The synchronization state.
*/
private volatile int state;
volatile凭借内存可见性,让写指令即时刷新到主内存,同时读操作会强制从主内存中读取,从这里我们可以隐隐想到,多个线程在并发操作时会围绕state状态来做下一步动作
2.2 Node内部类
先来看看Node内部类中的成员变量有哪些
java
/** 用于标记一个节点正以共享模式等待 */
static final Node SHARED = new Node();
/** 用于标记一个节点正以独占模式等待 */
static final Node EXCLUSIVE = null;
/** waitStatus 值,表示线程已取消 */
static final int CANCELLED = 1;
/** waitStatus 值,表示后继节点的线程需要被唤醒(通过 unpark) */
static final int SIGNAL = -1;
/** waitStatus 值,表示线程正在条件队列上等待 */
static final int CONDITION = -2;
/**
* waitStatus 值,表示下一次共享式获取操作应该无条件传播
*/
static final int PROPAGATE = -3;
/**
* 状态字段,取值仅为以下几种:
* SIGNAL: 此节点的后继节点(或即将)被阻塞(通过 park),因此当前节点在释放或取消时必须唤醒其后继节点。
* 为避免竞争,获取方法必须先表明它们需要信号,然后重试原子性获取操作,失败后再阻塞。
* CANCELLED: 由于超时或中断,此节点已被取消。节点永远不会脱离此状态。
* 特别地,具有已取消节点的线程永远不会再次阻塞。
* CONDITION: 此节点当前位于条件队列上。在转移之前,它不会用作同步队列节点,
* 转移时状态将被设置为 0。(这里使用此值与该字段的其他用途无关,但简化了机制。)
* PROPAGATE: 共享式释放操作应该传播到其他节点。此状态仅在 doReleaseShared 方法中为头节点设置,
* 以确保即使在其他操作介入的情况下,传播也能继续。
* 0: 以上情况都不是
*
* 这些值按数值排列是为了简化使用。非负值意味着节点不需要发出信号。
* 因此,大多数代码不需要检查特定的值,只需检查符号即可。
*
* 对于普通的同步节点,该字段初始化为 0;对于条件节点,初始化为 CONDITION。
* 它使用 CAS(或在可能的情况下,使用无条件的 volatile 写操作)进行修改。
*/
volatile int waitStatus;
/**
* 指向当前节点/线程依赖的前驱节点的引用,用于检查 waitStatus。
* 在入队时赋值,仅在出队时为了垃圾回收而置为 null。
* 此外,当前驱节点取消时,我们会在查找未取消的节点时进行短路操作,
* 因为头节点永远不会被取消:一个节点只有在成功获取锁后才会成为头节点。
* 一个已取消的线程永远不会成功获取锁,并且一个线程只会取消自身,不会取消其他节点。
*/
volatile Node prev;
/**
* 指向当前节点/线程在释放时要唤醒的后继节点的引用。
* 在入队时赋值,在绕过已取消的前驱节点时进行调整,在出队时为了垃圾回收而置为 null。
* 入队操作在附加节点之后才会给前驱节点的 next 字段赋值,
* 所以看到 next 字段为 null 并不一定意味着该节点是队列的末尾。
* 然而,如果 next 字段看起来为 null,我们可以从尾部扫描前驱节点来进行双重检查。
* 已取消节点的 next 字段会指向节点本身而不是 null,这是为了方便 isOnSyncQueue 方法的实现。
*/
volatile Node next;
/**
* 入队此节点的线程。在构造时初始化,使用后置为 null。
*/
volatile Thread thread;
/**
* 指向在条件队列上等待的下一个节点的引用,或者是特殊值 SHARED。
* 因为条件队列仅在以独占模式持有锁时才会被访问,所以我们只需要一个简单的链表队列来持有等待条件的节点。
* 然后这些节点会被转移到同步队列中重新获取锁。
* 并且由于条件只能是独占的,我们通过使用特殊值来表示共享模式,从而节省了一个字段。
*/
Node nextWaiter;
成员还是很多的我们逐个拆解来看看
Node SHARED
: 一个Node节点,用于标记一个节点正以共享模式等待,后面会详解共享模式Node EXCLUSIVE
:类似于前者,标记节点是独占模式int CANCELLED
;表示线程被取消int SIGNAL
:表示线程需要被唤醒int CONDITION
:表示线程处于等待队列上,后续会解释等待队列int PROPAGATE
:表示下次共享状态的传播逻辑volatile int waitStatus
:这里也是用volatile字段修饰,代表线程当前等待状态volatile Node prev
:前置节点volatile Node next
:后置节点volatile Thread thread
:入队的线程,在初始化的时候加入Node nextWaiter
:表示等待队列的下一个引用
2.3 AQS原理
了解完结构,这里画一张图帮助大家理解
AQS的原理并不复杂,AQS维护了一个volatile int state
变量和一个CLH(三个人名缩写)双向队列,队列中的节点持有线程引用,每个节点均可通过getState()
、setState()
和compareAndSetState()
对state进行修改和访问。
当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。
3. 队列转换逻辑
由上文结构我们可以推导出AQS内部FIFO队列结构,CLH队列和等待队列
- 同步队列:也称为 CLH 队列,是一个 FIFO(先进先出)的双向队列,用于存储等待获取同步状态的线程节点。当一个线程尝试获取同步状态失败时,会被封装成节点加入到同步队列的尾部,并进入阻塞状态,直到被唤醒。
- 条件队列 :是一个单向链表,用于存储调用
Condition
对象的await()
方法后进入等待状态的线程节点。当线程调用await()
方法时,会释放当前持有的同步状态,并将自己封装成节点加入到条件队列中;当其他线程调用Condition
对象的signal()
或signalAll()
方法时,会将条件队列中的节点转移到同步队列中,等待重新获取同步状态。
3.1从同步队列到条件队列(await()
方法)
当线程调用 Condition
对象的 await()
方法时,会触发从同步队列到条件队列的转换。
java
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 创建一个新的条件节点并加入到条件队列
Node node = addConditionWaiter();
// 释放当前持有的同步状态
int savedState = fullyRelease(node);
int interruptMode = 0;
// 检查节点是否在同步队列中,如果不在则阻塞
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 重新获取同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter()
:创建一个新的条件节点,并将其加入到条件队列的尾部。fullyRelease(Node node)
:释放当前线程持有的同步状态,并返回释放前的状态值。isOnSyncQueue(Node node)
:检查节点是否在同步队列中。如果不在,则线程进入阻塞状态,直到被唤醒。
3.2 从条件队列到同步队列(signal()
和 signalAll()
方法)
signal()
方法:将条件队列中的第一个节点转移到同步队列中。
java
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 尝试将节点的等待状态从 CONDITION 改为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将节点加入到同步队列中
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 ||!compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
doSignal(Node first)
:从条件队列的头部开始,尝试将节点转移到同步队列中,直到成功转移一个节点或条件队列为空。transferForSignal(Node node)
:将节点的等待状态从CONDITION
改为 0,并将节点加入到同步队列的尾部。如果节点的前驱节点状态为CANCELLED
或设置SIGNAL
状态失败,则唤醒该节点对应的线程。signalAll()
方法:将条件队列中的所有节点依次转移到同步队列中。
java
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
doSignalAll(Node first)
:遍历条件队列中的所有节点,将每个节点依次转移到同步队列中。
3.3 转换变量
上文Node节点有两个变量在队列转换中起到重要作用
Node nextWaiter
:
Node
在CLH
队列时,nextWaiter
表示共享式或独占式标记Node
在条件队列时,nextWaiter
表示下个Node
节点指针
volatile int waitStatus
:这里用图片表示一下
4.独占模式和共享模式
-
独占模式 :同一时刻只允许一个线程获取同步状态并执行临界区代码,其他线程需要等待该线程释放同步状态后才有机会获取。例如,
ReentrantLock
就是基于 AQS 的独占模式实现的,在同一时刻只有一个线程可以持有锁并执行被锁保护的代码块。 -
共享模式 :同一时刻可以有多个线程同时获取同步状态并执行临界区代码。例如,
Semaphore
(信号量)和CountDownLatch
就是基于 AQS 的共享模式实现的,允许多个线程同时访问有限的资源。
4.1 独占模式
独占模式底层有部分方法需要自己实现,因为ReentrantLock底层调用的AQS是独占模式,所以下文讲解的AQS源码也是针对独占模式的操作,ReentrantLock的加锁和解锁方法分别为lock()和unLock()
4.1.1 获取锁
lock()
方法 :这里是加锁的源头方法,逻辑很简单,线程进来后直接利用CAS
尝试抢占锁,如果抢占成功state
值回被改为1,且设置对象独占锁线程为当前线程,否则就调用acquire(1)
再次尝试获取锁。
java
final void lock() {
if (compareAndSetState(0, 1))
//设置持有锁线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- 继续执行
acquire(int arg)
方法:这是独占模式下获取同步状态的入口方法。
java
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire包含了几个函数的调用,
tryAcquire(arg)
:用于尝试获取同步状态。若获取成功则返回true
,失败则返回false
。addWaiter(Node.EXCLUSIVE)
:若tryAcquire
失败,会将当前线程封装成一个独占模式的节点(Node.EXCLUSIVE
),并添加到同步队列的尾部。acquireQueued
:使节点在队列中自旋等待获取同步状态,若期间被中断,会返回true
。selfInterrupt()
:若acquireQueued
返回true
,则当前线程会自我中断。
看下自定义的tryAcquire(arg)
方法
java
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
nonfairTryAcquire方法首先会获取state的值,如果为0,则正常获取该锁,不为0的话判断是否是当前线程占用了,是的话就累加state的值,这里的累加也是为了配合释放锁时候的次数,从而实现可重入锁的效果。
4.1.2 释放锁
release(int arg)
方法:用于独占模式下释放同步状态。
java
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(arg)
:代码上可以看出,核心的逻辑都在tryRelease
方法中,该方法的作用是释放资源,AQS里该方法没有具体的实现,需要由自定义的同步器去实现,我们看下ReentrantLock代码中对应方法的源码:
java
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease
方法会减去state对应的值,如果state为0,也就是已经彻底释放资源,就返回true,并且把独占的线程置为null,否则返回false。
unparkSuccessor(h)
:若tryRelease
成功,会唤醒同步队列中头节点的后继节点。
java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//将head结点的状态置为0
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点s
Node s = node.next;
//如果为空或已取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从后向前,直到找到等待状态小于0的结点,前面说了,结点waitStatus小于0时才有效
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 找到有效的结点,直接唤醒
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
方法的逻辑很简单,就是先将head的结点状态置为0,避免下面找结点的时候再找到head,然后找到队列中最前面的有效结点,然后唤醒,我们假设这个时候线程A已经释放锁,那么此时队列中排最前边竞争锁的线程B就会被唤醒。然后被唤醒的线程B就会尝试用CAS获取锁,回到acquireQueued
方法的逻辑
4.2 共享模式
4.2.1 获取锁
共享模式获取锁的顶层入口方法是acquireShared
,该方法会获取指定数量的资源,成功的话就直接返回,失败的话就进入等待队列,直到获取资源。
java
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
该方法里包含了两个方法的调用,
tryAcquireShared
:ryAcquireShared在AQS里没有实现,同样由自定义的同步器去完成具体的逻辑,像一些较为常见的并发工具Semaphore、CountDownLatch里就有对该方法的自定义实现,虽然实现的逻辑不同,但方法的作用是一样的,就是获取一定资源的资源,然后根据返回值判断是否还有剩余资源,从而决定下一步的操作。 这里以CountDownLatch为例
java
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
返回值有三种定义:
- 负值代表获取失败;
- 0代表获取成功,但没有剩余的资源,也就是state已经为0;
- 正值代表获取成功,而且state还有剩余,其他线程可以继续领取
当返回值小于0时,证明此次获取一定数量的锁失败了,然后就会走doAcquireShared方法
doAcquireShared
:进入等待队列,并循环尝试获取锁,直到成功。
java
private void doAcquireShared(int arg) {
// 加入队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
// CAS自旋
for (;;) {
final Node p = node.predecessor();
// 判断前驱结点是否是head
if (p == head) {
// 尝试获取一定数量的锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// 获取锁成功,而且还有剩余资源,就设置当前结点为head,并继续唤醒下一个线程
setHeadAndPropagate(node, r);
// 让前驱结点去掉引用链,方便被GC
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 跟独占模式一样,改前驱结点waitStatus为-1,并且当前线程挂起,等待被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// head指向自己
setHead(node);
// 如果还有剩余量,继续唤醒下一个邻居线程
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
其实两个流程并没有太大的差别,只是doAcquireShared()
比起独占模式下的获取锁上多了一步唤醒后继线程的操作,当获取完一定的资源后,发现还有剩余的资源,就继续唤醒下一个邻居线程,这才符合"共享"的思想
4.2.2 释放锁
共享模式释放锁的顶层方法是releaseShared
,它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源
java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared
:需子类实现,用于尝试共享式地释放同步状态。若释放成功则返回true
,失败则返回false
。
java
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
doAcquireShared
:若tryReleaseShared
成功,会唤醒同步队列中等待的线程,以保证共享状态的传播。
java
private void doReleaseShared() {
for (;;) {
// 获取等待队列中的head结点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// head结点waitStatus = -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;
}
}
如果等待队列head结点的waitStatus为-1的话,就直接唤醒后继结点,唤醒的方法unparkSuccessor()