一、概述
❄️ 什么是AQS
AbstractQueuedSynchronized(AQS),类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/ReadWriteLock等。
1.1 设计思想
AQS把为同步器提供了两个方法,acquire和release,acquire即获取资源,release即释放资源,伪代码如下: acquire:尝试获取资源,如果当前状态不允许获取资源,就进入队列,并阻塞等待,直到成功获取资源
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
release: 释放资源,如果释放后的状态,允许其他线程获取资源,则唤醒队列中的一个或者多个线程。
update synchronization state;
if (state may permit a blocked thread to acquire)
unblock one or more queued threads;
为了实现以上功能,AQS为需要提供一些基础机制,包括:
- 同步状态的原子性管理理;
- 线程队列;
- 线程的阻塞与唤醒。
稍后我们将简单介绍一下同步状态,而线程的阻塞/唤醒以及等待队列,则在原理那一章,详细描述。
1.2 同步状态
AQS使用一个volatile int类型属性(private volatile int state)标识同步器的状态,不同的同步器,该状态的含义不同。
- ReentrantLock:state代表独占锁的状态
- 0 标识没有线程占用该锁;
- 大于0 标识某个线程获取了该锁,具体的数字,代表重入的次数。
- Semaphore:state代表许可的数量
AQS提供了一些修改该状态的原子方法:
getState/setState: 获取/设置状态。compareAndSetState: 使用CAS的方式修改状态,成功返回true,否则返回false。
基于AQS实现的同步器,应该使用以上方法,来维护同步状态。
1.3 使用
AQS是抽象的,使用AQS的时候,一般不是直接继承,而是组合一个继承AQS的实现类。比如ReentrantLock就实现了公平和非公平的两种AQS。当然,也有直接继承的,比如ThreadPoolExecutor的Worker内部类。不过,一般建议创建一个内部类,继承AQS,实现相关方法,而在同步器内,进一步封装。
简单来说:同步器只要定义好state状态的含义,并实现维护状态的tryAcquire(Shared)/tryRelease(Shared)方法即可,其余的线程阻塞&唤醒和线程的等待队列,都交给AQS来实现。
AQS提供了几个方法,供子类复写,这些方法默认都抛出UnsupportedOperationException。其他方法都是final的,无法复写。
| 方法 | 描述 |
|---|---|
| protected boolean tryAcquire(int arg) | 独占模式。arg为获取资源的数量,尝试获取资源,成功则返回True,失败则返回False。 |
| protected boolean tryRelease(int arg) | 独占模式。arg为释放资源的数量,尝试释放资源,成功则返回True,失败则返回False。 |
| protected int tryAcquireShared(int arg) | 共享模式。arg为获取资源的数量,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 |
| protected boolean tryReleaseShared(int arg) | 共享模式。arg为资源的数量,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。 |
| protected boolean isHeldExclusively() | 是否被当前线程独占,在Condition条件队列中使用。 |
核心就是两种模式的tryAcquire(Shared)/tryRelease(Shared),翻译过来,就是尝试获取 & 释放锁,其实主要就是对state这个状态的维护。
❄️ 独占 & 共享
AQS提供了独占和共享两种模式,可以根据需要,选择实现其中一种甚至是两种(比如读写锁),如果仅需要一种模式,则仅实现对应模式的方法即可。
独占模式:资源仅能被一个线程独占使用。
- tryAcquire
- tryRelease
共享模式:资源可以被多个线程同时使用,比如Semaphore,CountDownLanch等。
- tryAcquireShared
- tryReleaseShared
另外,独占模式还有一个方法,isHeldExclusively(是否被当前线程独占),独占模式下Condition条件队列使用,不需要可以不实现。
❄️ 公平 & 非公平
AQS的线程等待队列,是一个先进先出的队列,即便如此,也不能保证线程获取锁的公平性。是否公平,需要看同步器是如何实现的。 回头看一下acquire的伪代码:
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
这里的synchronization state does not allow acquire,就是一次尝试获取资源,也就是tryAcquire,注意,这里是先尝试获取资源,无法获取资源后,才进入队列等待。因此一个新的 acquire 线程能够“窃取”本该属于队列头部第一个线程通过同步器的机会。所以以上这个伪代码的实现,就是可抢占的,非公平的同步器实现。
如果需要严格的公平性,可以把tryAcquire方法定义为,若当前线程不是队列的头节点(可通过 getFirstQueuedThread方法检查,这是框架提供的为数不多的几个检测方法之一),则立即失败(返回 false)。
一个更快,但非严格公平的变体可以这样做,若队列为空(判断的瞬间),仍然允许tryAcquire执行成功。在这种情况下,多个线程同时遇到一个空队列时可能会去竞争以使自己第一个获得锁,这样,通常至少有一个线程是无需入队列的。java.util.concurrent 包中所有支持公平模式的同步器都采用了这种策略。
❄️ Condition队列
AQS 框架提供了一个 ConditionObject 类,给维护独占同步的类以及实现 Lock 接口的类使用。一个锁对象可以关联任意数目的条件对象,可以提供典型的管程风格的 await、signal 和 signalAll 操作,包括带有超时的,以及一些检测、监控的方法。
与synchronized+Object的wait/notify(All)对比,Condition功能更加丰富:
- Condition提供了除可以响应中断的await方法外,还提供了,不响应中断的
awaitUninterruptibly(); - 同一个同步器,可以创建多个Condition,也就是说可以创建多个条件队列,而synchronized仅能有一个。
❄️ 简单的案例
我们来简单实现一个非可重入的互斥锁
一、定义state状态的含义,既然是非可重入的,我们简单粗暴定义:
- 0 : 无锁状态
- 1 :有锁状态
二、定义同步器:实现tryAcquire/tryRelease方法,除了维护state状态外,还要维护当前锁的独占线程。
private static class Sync extends AbstractQueuedSynchronizer {
// Acquires the lock if state is zero
public boolean tryAcquire(int acquires) {
//忽略acquires参数,固定为1。
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Releases the lock by setting state to zero
protected boolean tryRelease(int releases) {
//忽略acquires参数,固定为1。
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 是否被当前线程占用
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
// Provides a Condition
Condition newCondition() { return new ConditionObject(); }
}
三、在同步器的基础上,封装成锁
class Mutex implements Lock, java.io.Serializable {
// 上文定义的Sync,内嵌到该类中,这也是AQS推荐的使用方法。
private static class Sync extends AbstractQueuedSynchronizer {
}
// The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.getState()==1; }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
二、原理
❄️ 前置概念
AQS的基于以下原理实现,想要看懂AQS必须了解下面的概念
- volatile
- CAS
- LockSupport.park & unpark
想要看懂AQS,最好先看一下李狗哥的论文,这篇论文概要的解释了设计思想及性能,可以更好的帮助我们理解AQS。
2.1 CLH锁
AQS的核心,就是线程的等待队列,按照李狗哥的说法,这个队列是在CLH锁的基础上优化而来的(虽然最终感觉差了好多),可以先看一下CLH锁Java AQS 核心数据结构 -CLH 锁。
CLH的简单实现:
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class CLHLock {
public static class CLHNode {
private volatile boolean isWaiting = true; // 默认是在等待锁
}
private volatile CLHNode tail ;
private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
. newUpdater(CLHLock.class, CLHNode.class , "tail" );
public void lock(CLHNode currentThread) {
CLHNode preNode = UPDATER.getAndSet(this, currentThread);
if(preNode != null) {//已有线程占用了锁,进入自旋
while(preNode.isWaiting ) {
}
}
}
public void unlock(CLHNode currentThread) {
// 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
if (!UPDATER.compareAndSet(this, currentThread, null)) {
// 还有后续线程
currentThread.isWaiting = false ;// 改变状态,让后续线程结束自旋
}
}
}
说明: CLHLock定义了一个CLHNode节点,仅有一个volatile boolean isWaiting属性,代表是否等待。维护一个tail节点,初始为null,代表资源没有被占用。
- 某个线程获取锁,构造一个CLHNode节点,使用CAS把当前节点赋值给tail,同时获取上一个节点
- 上一个节点为null,获取锁成功;
- 上一个节点不为null,则自旋判断前一个节点的isWaiting属性,直到前一个节点的isWaiting属性为false。
- 某个线程释放锁,使用上一步构造出来的CLHNode
- 尝试把头节点,改为null,成功则释放锁成功;
- 失败说明当前节点后面还有其他线程等待,则把isWaiting改为false,让后面的线程结束自选。
PS:其实可以初始化一个isWaiting=false的头节点,这样实现上,少了很多null的判断。
CLH的缺点:
- 自旋操作,长时间等待,会有较大的CPU开销。
- 功能单一,不支持取消、超时等机制。
2.2 CLH改进
李狗哥在论文中讲,AQS是在CLH的基础上改进的,我们按照这个思路捋一捋。CLH最大的问题,就是自旋等待,如果持有锁的线程占用实现短还好,占用时间长,会有很大的无意义的CPU开销,性能非常差。因此,主要的改动,就是使用阻塞替换自旋。
那么,线程该如何阻塞呢?
2.2.1 线程的阻塞 & 唤醒 & 中断
参考:Java 多线程学习(7)聊聊 LockSupport.park() 和 LockSupport.unpark()
AQS使用LockSupport#park & unpark来支持线程的阻塞&唤醒。LockSupport为每个线程,关联一个“许可”,如果该许可可用,调用park方法,会消耗该“许可”,并立即返回,否则挂起,直到中断或者许可可用。使用unpark方法,可以给该线程发放“许可”,不过“许可”不会累计,最多就一个。
LockSupport常用的方法有:
- park():无限期挂起当前线程;
- parkNanos(long nanos):挂起当前线程一段时间;
- parkUntil(long deadline):在deadline之前一直挂起当前线程;
- unpark(Thread thread):唤醒thread线程(给thread方法许可)。
如果一个线程,已经持有“许可”,或者已经存在中断标记,则不会挂起,否则会挂起,直到:
- 其他线程调用了unpark方法,参数为被挂起的线程;
- 其他线程中断了被挂起的线程;
- 挂起的时间到了;
- 虚假的唤醒(The call spuriously (that is, for no reason) returns)。
这里讲的是虚假唤醒,可以参考以下几篇资料: Why does pthread_cond_wait have spurious wakeups? Do spurious wakeups in Java actually happen? 由于虚假唤醒的存在,在调用 park 时一般采用自旋的方式,伪代码如下:
while (!canProceed()) {
...
LockSupport.park(this);
}
==注意==
有两点值得注意:
- unpark可以在park之前被调用,发放“许可”,但不会累积,存在许可,调用park会消耗该“许可”,并立即返回。
- 如果中断标记,在调用park之前,已经存在,则park不会生效,并且,不会清除该中断。因此如果存在中断标记,必须手动清除该标记,才能park成功,这点与“许可”有很大的差别。
❄️ 补充中断相关知识
因为强制停止线程,可能会导致锁等相关资源不被释放,是一个很危险的事情。所以目前都使用中断的方式,软停止线程。这里要说一下isInterrupted和interrupted的区别:
- isInterrupted,直接调用isInterrupted方法,不带参数,就是返回中断标志,如果加一个参数,并且设置为true,则返回中断标志外,还会清空中断标志。不带参数的,其实就是执行了
isInterrupted(false); - interrupted(注意有ed),执行了
isInterrupted(true)。
一般,不响应中断的方法,在运行过程中,会把中断的标记清除掉,但运行结束的时候,需要恢复中断标记,比如acquire方法,如果期间中断了,会调用selfInterrupt,给自己来个中断,但是如果响应了中断,抛出了异常,一般,是会清除中断状态的,比如Thead.sleep方法,比如AQS的另外一个响应中断的acquireInterruptibly方法。
2.2.2 单向链表改为双向链表
CLH采用自旋的方式等待,只要监控之前节点的状态即可,无需其他线程唤醒。而阻塞等待,则必须依赖其他线程唤醒。
依赖谁呢?
当然是依赖活跃的线程,也就是当前持有资源的那个(共享模式下是几个)活跃的线程,依赖它(们)执行任务完成,释放锁,然后唤醒队列中的节点。
按照什么顺序唤醒呢?
当然是按照先进先出的原则,依次唤醒。但是CLH队列,通过CAS,实现的是一个从后向前的单向链表,无法高效的找到后继节点。因此李狗哥在这里做了一个优化,为每个节点增加一个指向后继节点next引用。
这里看下AQS的进入队列的源码。
- Node结构
- volatile Node pre/next:用于组成双向链表;
- volatile Thread thread :线程,用于Thread的后续唤醒;
- volatile int waitStatus :状态,后续细聊;
- Node nextWaiter : 用于Condition,非Condition,代表模式,后续细聊;
- AQS维护head/tail头尾节点
- 初始都为null,需要懒加载,懒加载在后面的
enq代码中会看到; - head初始化后始终为“哨兵”,即一个占位节点,因此head的下一个节点,才是队首节点。换句话说,判断一个节点是否是队首节点,需要判断pre == head。
- 初始都为null,需要懒加载,懒加载在后面的
以下是入队代码:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 仔细看这段代码,与enq的for循环下半部分代码一样,注释解释先尝试“抄小路”,失败走原路:
// Try the fast path of enq; backup to full enq on failure
// 这里比enq少了初始化代码,且仅尝试一次,确实少走了一点路,另外没有循环,jvm应该更容易优化。
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化
if (compareAndSetHead(new Node()))
tail = head;
} else { // CAS入队列
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
原子性如何保证?
无法保证:没有什么好办法,能保证一个双向链表的原子更新,AQS的实现中,也仅能保证pre引用的的正确性,next引用仅仅是靠t.next = node;这样的代码,简单的赋值。因此增加next引用,仅是一种帮助快速找到后继节点的优化。不过即使通过某个节点的next引用发现其后继结点不存在,总还是可以使用pred引用从尾部开始向前遍历来检查是否真的存在后继节点。
我们看下AQS的unparkSuccessor的部分代码,当next=null或者取消时,通过tail从后向前,找到后继节点。
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
//s.waitStatus > 0 标识取消,后续解释节点取消操作的时,会解释为什么有这个判断
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);
2.2.3 状态辅助
论文中有一段描述,节点不需要有是否释放的状态,只要判断当前节点是否为队首即可,这里解释一下:
CLH队列每个线程,自旋判断前一个节点是否释放的状态,只要前一个节点释放了锁,当前节点就可以获取到锁。CLH这个逻辑,是在简单的自旋锁上的一个优化。简单的自旋锁,是没有链表的,直接自旋判断单一节点的状态,竞争激烈的情况下,锁状态变更会导致多个CPU的高速缓存的频繁同步,从而拖慢CPU效率。 AQS是基于阻塞&唤醒,当一个节点被唤醒后,不需要关心前一个节点是否释放锁,只要判断当前节点是否是队列的第一个节点,是,就可以调用子类的tryAcquire方法尝试获取锁(可能会失败,重新阻塞)。
2.2.3.1 SIGNAL(-1)状态
线程调用子类定义的tryAcquire失败后,会进入队列,然而,进入队列后,就可以放心阻塞等待了么?
并不能,因为入队列与资源释放可能并发,也就是当前线程进入队列过程中,可能恰好持有资源的线程正在释放,释放时队列还是空的。如果入队后直接阻塞,就可能无法被唤醒。因此,即使进入队列后:
至少还要重新判断一下,是否是队列中的第一个节点,是的话还要调用
tryAcquire尝试获取锁,再次失败后,才能放心阻塞等待。
理论上,按照上述逻辑实现,应该就没有问题。但是李狗哥为了减少线程频繁的阻塞&唤醒,为每个节点增加了一个SIGNAL(-1)状态。这里约定:
- 线程释放资源之后,只有head节点为SIGNAL(确切的说,是 < 0,后续讲共享锁 时细聊),才会试图唤醒队列中的节点。
- 线程进入队列后,必须保证前一个节点的waitStatus为SIGNAL,才会试图阻塞等待。
与进入队列后不能立即阻塞原因一样,修改成功后,也不能立即阻塞,
至少还要重新判断一下,是否是队列中的第一个节点,是的话还要调用
tryAcquire尝试获取锁,再次失败后,才能放心阻塞等待。
我们看下独占锁不响应中断的加锁源码:
/**
* 调用需要子类实现的tryAcquire方法,尝试获取资源,如果失败:
* + 调用addWaiter创建节点入队列;
* + 调用acquireQueued尝试阻塞等待资源,该方法记录执行过程中是否中断,曾中断,返回true,用于中断标记的恢复
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 虽然不响应中断,但是中断的标记需要恢复
}
/**
* addWaiter代码之前已经分析过,就是进入队列,进入队列后,返回的Node在该方法内尝试阻塞等待锁。
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 进入这段代码有几个场景:
// 1. 刚进入队列,尚未阻塞,会先在这里判断是否队首节点,是则尝试获取锁
// 2. 后面的shouldParkAfterFailedAcquire返回false又循环一次;
// 3. 后面的parkAndCheckInterrupt(阻塞)后被唤醒 或 中断;
// 如果是第一个节点(head是哨兵,node的前一个节点是head,则node是第一个节点),尝试获取锁
if (p == head && tryAcquire(arg)) {
// 成功后,出队列,也就是把自己设置成head
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted; // 返回是否发生中断
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断是否可以阻塞等待
parkAndCheckInterrupt())// 阻塞等待,且不响应异常,如果发生异常返回true
interrupted = true;
}
} finally {
// 锁获取失败,取消等待,可能是tryAcquire失败,中断或超时(这个方法不会超时,也不会中断),其他方法里有这种可能
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 已经成功设置前一个节点的状态为signal了,返回true
return true;
if (ws > 0) {
/**
* 跳过取消节点
* 注意这里pre修改与next修改不是原子的,链表在某个时刻,pre和next可能是不一致的。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/**
* CAS方式更新pre的waitStatus为SIGNAL。
* 注意这里没有return cas结果,因为即使成功了,也要判断是否该再尝试一遍。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
/**
* node设置为head,也就是出队列,在线程进入队列后成功获取到资源后调用
*/
private void setHead(Node node) {
head = node;
// 注意这里,后续讲取消时会用到。
node.thread = null;
node.prev = null;
}
独占锁解锁源码: 同样tryRelease留给子类复写,如果成功释放锁了,会根据尝试唤醒后继节点。唤醒前会查看head的状态,如果状态不为0,才会尝试唤醒。前文已经说过,这是一个优化手段,防止不必要的park和unpark带来的开销,提高性能。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* 把当前节点的状态改回0,可以结合shouldParkAfterFailedAcquire
* 和acquireQueued代码结合来看,极有可能后面的节点刚改为 SIGNAL,
* 又在这里被改回0,后面的节点多了一次park前在acquireQueued中获取
* 锁的机会,这应该是李狗哥的本意,毕竟park和unpark,也就线程的阻塞
* 和唤醒——线程切换——是比较耗费性能的。
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
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);
}
2.2.3.2 PROPAGATE(-3)状态
因为当多个线程并发执行releaseShared时,有可能出现在AQS同步队列等待的节点,比如前一个线程完成了释放唤醒,同时后一个线程获取锁,但还未执行setHeadAndPropagate进行共享锁传播,也就是未设置好head,也就是说此时读取老的head状态为0(也就是这种else if (ws == 0情况),会导致释放但不唤醒,此时会将头结点的状态置为 PROPAGATE状态,让获取锁的线程任然能够进行共享锁传播,唤醒下一个线程。
参考: AQS 中 Node.PROPAGATE 状态引入的意义 面试官问我 AQS 中的 PROPAGATE 有什么用?
PROPAGATE状态解决的bug。
bug: bugs.openjdk.java.net/browse/JDK-… fix: github.com/openjdk/jdk…
上文分析了独占锁的获取/释放锁的源码,接下来看下共享锁的获取/释放源码:
/**
* 调用子类的tryAcquireShared方法,失败了,调用doAcquireShared,尝试入队等待
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 共享版本的acquireQueued
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//模式为Node.SHARED(共享)
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//重点区别,获取锁成功后,调用的是setHeadAndPropagate,而非setHead
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);
}
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
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;
}
}
简单过下代码,与acquireQueued区别不多:
- 入队列时,节点的状态为Node.SHARED模式,这点没什么好解释的。
- 获取锁成功后,调用的是setHeadAndPropagate,而非setHead。这是核心区别,独占模式下,获取锁成功后,退出队列即可,因为不可能有其他线程,能够成功获取锁了;共享模式下不同,还可以,因此需要向后传播(Propagate)。
接下来,看下setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 注意这里都是或,也就是说,即使propagate<=0,只要head(不管是旧的还是最新的)的状态<0,就会doReleaseShared
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
释放共享锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {//队列不为空
int ws = h.waitStatus;
// 状态为SIGNAL,唤醒后继节点
if (ws == Node.SIGNAL) {
// CAS失败重新循环(head不变的话,下次循环进入else)
// 毕竟有一个线程unparkSuccessor就可以
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 把状态改为PROPAGATE,告知其他节点,有新资源释放,无条件唤醒
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头结点改变了,继续循环,这里为了加速唤醒
if (h == head) // loop if head changed
break;
}
}
2.2.4 取消
前文在acquireQueued代码的finally中,有个判断,如果失败,则取消,也就是退出等待队列。这个判断逻辑,在AQS提供的各种acquire方法中,进入队列后,都有类似的逻辑。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
...
} finally {
if (failed)
cancelAcquire(node);
}
}
在哪种场景下,会取消呢:
- tryAcquire失败,子类实现的,可能有问题。
- 超时后未获取到锁
- 中断后未获取到锁
这里超时和中断需要对应的方法支持:
doAcquireInterruptibly可以响应中断;doAcquireNanos可以设置超时时间,也响应中断。
所谓取消,似乎只是在队列中删除该节点,但,真如此简单么? 并不是,因为释放资源与节点取消可能并发。释放资源的线程unpark了正在取消的节点,如果该取消节点仅仅删除自己,会导致后续的节点无法被唤醒。
因此在取消逻辑首要任务,是必须保证后续节点(如果有)可以被正确的唤醒。AQS的取消策略,是相对保守的,核心就是:
- 把自己的Node状态改为CANCELLED,这样其他节点可以跳过该节点;
- 必须确保其他线程可以唤醒后继节点,否则主动唤醒后继节点; 下面我们看下代码:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 注意这里设置线程为null
// 不用担心这个在改状态之前,因为LockSupport.unpark(null)不会抛出异常
node.thread = null;
// 跳过取消的前置节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 当前节点前面第一个非取消节点的后置节点,用作后面的CAS
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
// 在这个原子赋值操作之后,其他节点会跳过本节点,之前的操作,不受其他线程干扰。
node.waitStatus = Node.CANCELLED;
// 如果是尾节点(没有后继节点需要唤醒,删除也无所谓),尝试删除自己,删除失败,进入else
if (node == tail && compareAndSetTail(node, pred)) {
// 是尾节点且删除成功,把pre的next改为null,失败也无所谓
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
// 存在后继节点需要唤醒,尝试把前置节点的next指向后继节点(但必须保证前置节点的waitStatus为SIGNAL)
int ws;
if (
//是第一个节点,立即进入else(因为如果是第一个节点,本节点可能已经被错误的唤醒了)
pred != head &&
// 前一个节点的状态是SIGNAL || 修改为SIGNAL成功
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
// pred.thread不为null
// - 即使pred节点正在取消,也在执行`node.thread = null`代码之前
// - 即使pred节点为head节点,也在执行setHead方法的`node.thread = null`代码之前(注意在并发环境下,第一个pred != head并不能保证在这里也成立)
pred.thread != null) {
// 运行到这里,说明pre节点,可以承担唤醒后继节点的责任。
// 至于下面的修正pre.next节点的操作,CAS失败了,也无所谓。
Node next = node.next;
if (next != null && next.waitStatus <= 0) //后面的节点没有取消
compareAndSetNext(pred, predNext, next);
} else {
// 否则,主动唤醒一下后继节点
unparkSuccessor(node);
}
// 注意这里为了帮助GC,把next指向了自身
// 因此unparkSuccessor代码中,next节点,如果为取消,也会通过tail从后向前查找未取消节点。
node.next = node; // help GC
}
}
2.2.5 Condition - 条件
AQS的内部类ConditionObject实现了Condition的功能——获取独占锁之后,如果不满足某个条件,则等待并释放锁,直到条件满足后,再被唤醒。这和Object的wait/notify/notifyAll提供的功能类似。相对于synchronized+wait/notify,Lock+condition有几个优点:
- 支持多个condition,这是个很重要的特性
- 支持不响应中断的wait——awaitUninterruptibly
注意condition只支持独占锁。
概述
可以先猜想一下,Condition的wait的大致流程:
- 一定要先释放锁
- 把当前线程包装成节点放到等待队列中(实现的时候这个在第一个步)
- 阻塞等待
- 被唤醒后尝试去获取锁
Condition也要维护一个队列,这个队列是一个单向链表,前面已经说过AQS的队列与Condition队列的节点数据结构是一样的,都是那个Node,nextWaiter就是在这里应用的,而在AQS用来表示是独占还是共享模式。
为什么一定要用一个数据结构
在Condition队列中,被唤醒之后,要尝试获取锁,获取锁的方式,就是进入AQS的等待队列中,数据结构一致,不需要转化,编码方便一些。
Condition的next为什么不直接使用next,非要新添一个属性,nextWaiter
Condition队列唤醒后,要进入AQS的等待队列,这里都是有并发的,如果复用next,那么这个next指向的到底是AQS中的等待队列,还是Condition中的呢?难以判断,因此必须重新定义一个。而李狗哥还把这个nextWaiter在AQS中当做模式来使用。
await - 释放锁,并等待某个条件唤醒
上代码:
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 节点加到队列中
Node node = addConditionWaiter();
// 释放资源,并记录资源数量,如果当前没有占有锁,fullyRelease会抛出异常
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);
}
实际实现中,是把节点先加入了Condition等待队列,然后才释放锁的,颠倒一下可以么?
看addConditionWaiter的代码就知道了,不可以,因为addConditionWaiter没有任何的CAS,非线程安全的,靠的就是先加入节点,后释放锁。Condition是独占锁才有的特性,这一步一定是已经获得了锁,那么没释放锁之前,addConditionWaiter一定是单线程的,不用担心并发问题。
addConditionWaiter - 添加条件队列节点
addConditionWaiter很简单,单向链表的追加,因为是单线程的没有并发问题。
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 顺便删除取消节点
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
checkInterruptWhileWaiting - 检查等待过程中是否发生中断
释放资源(锁)的代码比较简单,就不看了,唯需要注意的就是,一定要记录自己获取数量,因为后面重新获取锁的时候,还要用到。
释放资源(锁)之后,当前线程进入循环,开始等待。唤醒后检查唤醒原因:
- 有其他线程signal了当前节点
- 没有线程signal,等待超时了(调用超时的版本)
- 被中断
如果是被中断了,就不但要重新获取锁,还要抛出异常,如果是awaitUninterruptibly方法调用的,则不抛出异常,但是要恢复自身的中断标记。
被中断其实非常好判断,看当前线程的中断标记就可以。但是假如中断与signal并发了呢?如何判断到底是谁先发生的呢?
老方法,CAS改状态,无论怎样,只有一个线程能改成功。改的当然是当前Node节点的status状态。都会把状态从CONDITION改为0,谁改成功算谁的。
checkInterruptWhileWaiting - 检查等待过程中是否发生中断源码
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? // 没发生中断返回 0 ,说明被singal了
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :// 发生中断,尝试把当前Node转移到AQS队列中,成功返回THROW_IE,否则返回REINTERRUPT
0;
}
发生中断,尝试把当前节点追加到AQS等待队列:
final boolean transferAfterCancelledWait(Node node) {
// 中断改成功,则认为先发生的中断,当前node进入AQS的等待队列,返回true,应该抛出异常(THROW_IE)
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
// 否则说明其他线程先唤醒的,等待其他线程把当前node放进AQS队列,返回false,恢复中断(REINTERRUPT)
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
重新获取锁,并处理中断
回到那个循环,无论是什么原因唤醒,Node都会转移到AQS的队列中,会跳出这个循环:
while (!isOnSyncQueue(node)) { // 直到当前节点在AQS队列中,跳出循环
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
接下来就用调用acquireQueued,重新获取上面释放的锁,如果在上面的while中没有发生中断,但是在acquireQueued中发生了中断(发生中断acquireQueued返回true),处理中断的模式为REINTERRUPT。
// 重新获取锁,获取资源 数量与上面记录的一致
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;//acquireQueued发生中断,且上面while等待没有发生中断,则改为REINTERRUPT,下面会恢复中断标志
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 恢复异常或中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
reportInterruptAfterWait很简单,就是THROW_IE抛出异常,REINTERRUPT恢复中断标志。
unlinkCancelledWaiters看完signal方法后再看。
signal & signalAll 条件成立,唤醒Condition的等待的线程
signal与signalAll对应的就是对象的notify与notifyAll方法。就是唤醒一个线程,还是所有线程。
题外话,什么时候用signal,什么时候用signalAll
只有满足下面条件的才调用signal,否则调用signalAll
- 所有等待线程拥有相同的等待条件;
- 所有等待线程被唤醒后,执行相同的操作;
- 只需要唤醒一个线程
signal
signal,唤醒队列中的第一个节点,其实就是把这个节点转移AQS的等待队列中,等待获取锁,并不是真正的唤醒。当然如果放进去的时候,没有把前一个节点的状态成功改为SIGNAL,会主动唤醒一下。
看代码:
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);
}
放回到AQS等待队列的代码,这里可能会与取消的场景冲突(超时或中断)
final boolean transferForSignal(Node node) {
// 在这里与中断或者超时竞争,失败这说明该节点先被取消(中断或超时)了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 把节点放到AQS队列中,并设置前面节点为SIGNAL;
// 成功,则该node的线程,会在AQS队列中等待,前面节点释放锁后唤醒他
// 失败,此处就唤醒他,让他尝试获取锁
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
signalAll
signalAll就是唤醒队列中所有节点,看明白上面的代码,这里就很简单,就是循环一下:
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);
}
unlinkCancelledWaiters - 删除取消节点
signal或者signalAll都会先把节点从Condition中删除,才会放到AQS的队列中。而transferAfterCancelledWait方法则不会,也就是无论是超时还是中断,都不会先删除,在追加,而是只改了Node的状态。
为什么呢?
无论是signal还是signalAll,都是必须已经占有锁,才能调用,对应的代码是单线程的,不会有并发问题,可以删。然而,transferAfterCancelledWait方法,则不能,是会并发的,所以仅仅是设置了状态,而没有删除。
那在什么时候取消节点呢?
等到单线程的场景下删除。看过之前的await代码能够可以知道,在await释放锁之前的addConditionWaiter时会调用unlinkCancelledWaiters,之后重新获取锁后,还会在调用一次unlinkCancelledWaiters方法。
unlinkCancelledWaiters源码,单线程,很简单:
/**
* Unlinks cancelled waiter nodes from condition queue.
* Called only while holding lock. This is called when
* cancellation occurred during condition wait, and upon
* insertion of a new waiter when lastWaiter is seen to have
* been cancelled. This method is needed to avoid garbage
* retention in the absence of signals. So even though it may
* require a full traversal, it comes into play only when
* timeouts or cancellations occur in the absence of
* signals. It traverses all nodes rather than stopping at a
* particular target to unlink all pointers to garbage nodes
* without requiring many re-traversals during cancellation
* storms.
*/
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}