AQS(一)独占锁(基于JDK 8)
@
1 介绍
在开始之前,先推荐一本书和两个博客,书是《Java并发实现原理:JDK源码剖析_出版社 电子工业出版社; 第1版》,可从我同名公众号下载。博客是 https://segmentfault.com/a/1190000016058789 和 https://blog.csdn.net/anlian523/article/details/106598910。
AQS(AbstractQueuedSynchronizer) 是 Java 很多并发框架的基础,主要数据结构是两个队列,双向同步队列CLH 和一个单向条件队列。只有同步队列中的节点才能获取锁。同步队列的唤醒指的是解除堵塞 unpark,条件队列中所谓的唤醒是把节点从条件队列移到同步队列,让节点有机会去获取锁。
利用AQS 的类有如下几个,独占锁 ReentrantLock,共享锁 Semaphore 和 CountDownLatch,写独占,读共享锁 ReentrantReadWriteLock,另外 ThreadPoolExecutor 中的 Worker 也实现了AQS。
对于几个锁来说,实现方式都是一样的,有一个内部类 Sync,继承了 AQS,锁内有成员变量 sync。如下图所示,几个锁就是下面的 LockClass。
1.1 state
state 在各个锁含义不同。
在 ReentrantLock 中,state 为 0表示当前没有锁,大于0表示当前已经有线程拥有锁,且重入了 state 次。在获取锁时,state 增加;释放锁时,state 减少。
在 Semaphore 中,state 表示许可证数目,获取锁减少,释放锁增加。
在 CountDownLatch 中,state 表示倒计时数目,释放会减少。
1.2 Node
Node 是 AQS 的内部类,有着五种状态 waitStatus,用于 sync queue 中节点的前后指针 prev 和 next,内部线程 thread 和 nextWaiter。
Node用来构建同步队列节点,nextWaiter标识同步锁是独占锁还是共享锁;Node用来构建条件队列节点,nextWaiter指向单向链表下一个节点。
最重要的是五种状态:CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
此外要注意 WaitStatus > 0 暗示了就是 CANCELLED。
static final class Node {
// 同步队列节点中标识是独占锁还是共享锁
/** 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;
// 下面是5种状态
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//省略了一种情况,即初始化时,为0
volatile int waitStatus;
// sync queue 的两个指针
volatile Node prev;
volatile Node next;
volatile Thread thread;
// 不同队列中的节点,含义不同
// 这里表示是独占锁还是共享锁
Node nextWaiter;
在 AQS 中,需要记录双向队列的头 head 和尾 tail。
// 头结点,thread成员固定为null,要处理的是下一个节点
private transient volatile Node head;
// 尾节点,请求锁失败的线程,总是放到队尾
private transient volatile Node tail;
1.3 模板方法
下列方法在 AQS 中会直接抛出异常,如果需要自己的类实现 AQS,只需要重写下面的方法即可。
方法名 | 描述 |
---|---|
protected boolean isHeldExclusively() | 该线程是否正在独占资源。只有用到Condition才需要去实现它。 |
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。 |
1.4 其他
// 对于独占锁来说,如果有线程持有锁,则记录该线程
// 可以理解为是现在 head 节点的线程,拿出来清空以后放在这里
// 这就是获取锁,且正在执行的线程
private transient Thread exclusiveOwnerThread;
// CAS相关,使用时看名字就行
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}
private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}
2 锁的获取 acquire
在 AQS 的分析中,需要先把流程理顺,然后再具体分析。不需要考虑其中的 1.3 的模板方法是怎么实现的,假定已经实现了功能即可。
对于下面的方法,总是先简单尝试一下,如果不行,会进行更复杂的自旋保证成功。
详细来说 :首先尝试获取(tryAcquire),失败后将当前线程包装到一个 Node 中,插入同步队列尾部(addWaiter),并在队列中不断自旋尝试(acquireQueued),满足堵塞条件(shouldParkAfterFailedAcquire)则会堵塞(parkAndCheckInterrupt)以减轻自旋的 CPU 消耗,如果堵塞后被唤醒会继续自旋尝试,直到成功后返回,并根据中断情况设置中断(selfInterrupt);或者因为异常抛出没有成功,则取消该线程所在的节点(cancelAcquire)。
2.1 acquire
首先尝试一次 tryAcquire,成功则说明锁已经获取,结束;失败则需要继续处理,做法是使用 addWaiter 将当前线程插入同步队列尾部,然后使用 acquireQueued 在队列中完成锁的获取。acquireQueued 返回值表示在执行期间是否被中断,执行期间不响应中断,只是把中断状态记下来。返回 true 表示被中断过,使用 selfInterrupt 来中断即可。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 中断自己
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
2.2 addWaiter
新建一个节点 node,先尝试直接入队,失败的话使用 enq 入队。
/**
* 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;
// 将 node 放入尾部,有三步
// 下面先修改前指针prev,设置尾部成功后,再建立后指针pred
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 前面无法成功,需要enq入队
enq(node);
return node;
}
// 这里说明在CLH队列中,nextWaiter就是表示独占或者共享
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
2.3 enq
enq 保证入队成功,如果 t 为 null,说明是初始化,新建一个空节点 new Node 放在头部,并设置 tail = head;
否则,试图将 node 放入尾部,有三步,修改 node 和原尾部 t 的指针,CAS 修改尾部。
和 addWaiter 一样,先修改 node 的前指针,然后 CAS,然后修改 t 的后指针。
/**
* 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;
}
}
}
}
2.4 acquireQueued
最重要的方法。
使得在队列中的 node 获取独占锁,使用 for 自旋和堵塞/唤醒来实现,在不抛出异常的情况下一定会成功,返回堵塞期间是否被中断。
在 for 循环中,先尝试获取 node 前一个节点 p,如果 p 是 head,执行 tryAcquire,成功的话,做一些操作并返回;失败的话,会检查 shouldParkAfterFailedAcquire,如果返回 true,表示可以堵塞,执行 parkAndCheckInterrupt,堵塞线程,直到其他线程唤醒该 node,唤醒后继续执行 for 循环。
如果存在抛出异常的情况,finally 中的 if 为 true,则取消 node。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取之前的节点
final Node p = node.predecessor();
// 如果前一个是head,尝试一次 tryAcquire
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 返回中断情况
return interrupted;
}
// should... 返回能否堵塞,如果能,执行park...
// park... 的返回堵塞期间是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 异常退出,失败,取消获取
if (failed)
cancelAcquire(node);
}
}
// 不修改节点的状态 waitStatus
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
// Node的方法
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
2.5 shouldParkAfterFailedAcquire
should Park After Failed Acquire,意思是在失败后应该堵塞吗?返回 true表示应该堵塞,接下来执行 parkAndCheckInterrupt。
-
node 前一个节点是 pred,如果 ws 是 SIGNAL,根据 1.2 中 SIGNAL 的含义,后续的 node 等待 pred 唤醒,这些已经完成了,所以返回 true。
-
ws >0 表示是 CANCELLED,会不断地往前找,找到第一个未取消的节点(pred.waitStatus<=0),设置为新的 pred。
-
否则,尝试将 ws 修改为 SIGNAL。后两种情况返回 false。
在多线程中,由于节点不断地修改,可能会多次返回 false。下面假定其他线程的修改对两个节点没有影响,分析一下需要多少次返回 true:
-
在第一个 if 中,直接返回 true;
-
在第二个 if 中,会先找到一个未取消的新节点作为 pred,下一次进入该方法有两种情况,一个是第一个 if ,返回 true,另一个是 else 的情况;
-
在 else中,本次会尝试修改ws,如果修改成功,则下一次返回 true,否则下次再次进入 else 修改。
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
// 如果 node 前一个节点状态已经是 SIGNAL,可以堵塞了,返回 true
// 其余情况是尽量往 SIGANL 靠,返回 false,在多次进入方法后有望变成 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;
// 等价于 else if (ws > 0)
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;
}
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// LockSupport.java
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
// park后不会释放,等待unpark
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
2.6 cancelAcquire
该方法是在 acquireQueued 失败后调用的,目的是从队列中删除队列中的 node,或者直接唤醒 node 后续节点。
1 比较复杂的地方是处理 node 非 tail 时的操作: if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null)
,可以分解为三部分:
① pred != head
② ((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
③ pred.thread != null
也就是 (pred 不是 head) &&(ws 是 SIGNAL 或者 ws 不是 CANCELLED,然后尝试修改为 SIGNAL 成功) && (pred 的线程不是 null)
2 最后一句node.next = node;
看不懂。
/**
* Cancels an ongoing attempt to acquire.
*
* @param node the node
*/
// 在获取失败后删除队列中的 node,或者直接唤醒 node 后面的节点
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
// 从node向前查找,找到第一个非取消的
// 作为 pred
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
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 设置为 CANCELLED
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// node 在尾部,只需要将 pred移动到尾部,pred.next 修改为 null即可
if (node == tail && compareAndSetTail(node, pred)) {
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.
int ws;
//比较复杂,见上面解释
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
// 尝试将 pred.next 修改为 next,也就是删除了node
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 直接唤醒 node 下一个节点
// 方法分析见第三节
unparkSuccessor(node);
}
// 这一步看不懂??
node.next = node; // help GC
}
}
3 锁的释放release
流程如下:尝试释放(tryRelease),成功后解除 head 后一个节点(unparkSuccessor),并返回 true;失败则返回 false 。
注意到 release 只执行一次,而 acquire 内的 acquireQueued 会自旋和堵塞,直到成功。这是因为如果当前线程持有锁,释放是没有竞争的。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.1 unparkSuccessor
唤醒 node 的下一个非 CANCELLED 节点,unpark 节点的线程。
有一个难点是如果 s 不满足条件,考虑从尾向前查找,这是因为前面的尾分叉操作,原因在 3.2 详述。
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* 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.
*/
// 这里的目的是将node的状态恢复为初始0,当然失败也无所谓
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* 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 下一个非空非CANCELLED 节点
// 如果 next 不满足条件,则尝试从尾向前找
// 原因见 3.2
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;
}
// 解除堵塞,线程会从 acquireQueued 的 parkAndCheckInterrupt
// 继续运行
if (s != null)
LockSupport.unpark(s.thread);
}
3.2 从尾遍历的原因(尾分叉)
在 addWaiter 和 enq 中,会发现向尾部插入的时候,总是先修改前指针 prev,CAS 设置尾部成功后,再修改后指针 next,正是因为两个节点先修改的前指针,所以如果前指针 prev 正确,后指针 next 不一定正确甚至不存在,只能从 tail 不断利用前指针往前查找。
/**
* 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;
// 将 node 放入尾部,有三步
// 下面先修改node前指针prev,设置尾部成功后,再建立原尾部后指针next
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 前面无法成功,需要enq入队
enq(node);
return 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 {
// 和 addWaiter 中一模一样
// 下面先修改node前指针prev,设置尾部成功后,再建立原尾部后指针next
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
下面以两个线程为例,如果有两个线程执行了 node.prev = t,接下来线程1 CAS 成功,会发生什么。
4 其他方法
上面只介绍了最普通的获取和释放方法,没有超时,对于中断会记下来而不在中间响应。这两个方法被 ReentrantLock 的 lock/unlock 使用。
另外还有一些其他的方法,能够中断或者超时返回。具体使用的时候看名字就行。下面举几个例子,简单看一下。
//在处理完成之后才会根据是否有中断决定是否执行 selfInterrupt
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//如果有中断,会直接抛出异常
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);
}
}
// 除了对中断抛出异常外,还会根据时间,超过时间返回 false
static final long spinForTimeoutThreshold = 1000L;
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();
// 此时会失败,在finally 处取消
if (nanosTimeout <= 0L)
return false;
// 这里是说,只有纳秒数nanosTimeout超过1000,才有堵塞的必要,
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 堵塞 nanosTimeout 这么长时间
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 返回中断情况 boolean ,由上层自行处理
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);
}
}