J.U.C并发框架之AQS(二):ReentrantLock释放锁

当一个线程执行结束,是怎么唤醒后续线程的?一个线程cancel后是怎样出队的?上一篇主要分析了获取锁,这一篇将聚焦释放锁以解答自己的困惑。本篇还是承接《J.U.C并发框架之AQS(一):ReentrantLock获取锁》的实验用例,锁限定为非中断且非公平且独占的情形,而中断锁、非公平锁以及独占锁的研究将在后续开专篇解析。

执行lock.unlock()释放锁

当Thread-0执行到方法体的lock.unlock()语句时,预示着该线程将释放锁并运行结束。unlock()是这样一个方法:

1
2
3
public void unlock() {
sync.release(1);
}

release()如下:

1
2
3
4
5
6
7
8
9
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)都发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

该方法releases被传入实参1,由于当前同步器的状态state = 1,所以c等于0,会执行第二个if语句的操作,但是脱离本实例来说,这里的锁是可重入的,所以也可能最后返回的free为false,不释放锁。

继续跟着实验用例分析,在tryRelease(arg)返回true后,程序向下运行,来到一个if语句,判断h是否为null以及h.waitStatus是否等于0。通过上一篇分析可知,head更改默认赋值,通常是在出现在第二个线程后,所以如果原本程序就只有一个线程在执行(这里不包括主线程以及守护线程等),那么也不存在唤醒后继节点线程的操作了,同样的,h.waitStatus如果等于0,那么要么没有后继节点,要么后继节点不需要唤醒。

unparkSuccessor方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.
*/
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 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);
}

可以看出当执行了unparkSuccessor方法后,如果节点的ws小于0,会先通过CAS将值设置为0,然后获得node.next并赋值给s。如果s为空,或者s的状态大于0处于cancel状态,那么就从等待队列的尾节点开始,查找第一个ws小于等于0的节点,只要这个节点存在,就调用LockSupport.unpark()将其唤醒。

至此一个正常的释放锁过程就解析完了。

cancelAcquire()

同步器内的Node持有CANCELLED字段,默认设置为1,是唯一一个大于0的waitStatus值。之前的解析多次考虑到ws大于1的情形,在AQS里,CANCELLED的设置出现在两个方法中,一个是fullyRelease(),一个是cancelAcquire(),前者主要出现在Condition的实现类,所以这里主要研究cancelAcquire(),且本文开头提到,先考虑研究独占、非中断、非公平锁的情形,也暂不考虑tryLock(),后续将用专门的篇章将这些情形补全。

cancelAcquire方法的代码不是特别长,直接放上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
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.
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;
// If we are the tail, remove ourselves.
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;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}

如果还没创建代表当前线程的节点,自然就直接返回了,如果结点不为空,首先将它内部的thread字段置空,然后就是出队的操作了,不断向前查找第一个waitStatus不大于0的节点,设其为pred,通过pred.next获得它的下个节点,实际上这个节点waitStatus大于0应该cancel,没错,这里获取它是为了后续的CAS操作。

如果传入的节点node是尾节点,那么之前通过不断寻觅获得的第一个waitStatus不大于0的节点pred之后的节点,都应该被移除,移除方法很简单,直接将pred设为尾节点,然后将其next节点置null就行了,在源代码中,这两步采用CAS的方式实现。

如果传入的节点node不是尾节点,那就要考虑要不要唤醒node.next节点的线程。试想,如果pred正好是head节点,就将node.next节点的线程唤醒,让它尝试获取同步锁,这种情形就变成了acquireQueued方法for循环内一开始调用的情形!如果pred引用的线程为null,则也应该唤醒。除此之外,node.next节点的线程可以不需要唤醒(这里之执行unparkSuccessor(node)操作),只需要将pred的next字段指向node.next节点,而pred的ws如果不是Node.SIGNAL,还要将其设置为Node.SIGNAL。

小结

本篇主要分析了ReentrantLock释放锁的过程,还解析了cancelAcquire方法,该方法通常出现在finally语句中,AQS将tryAcquire方法的具体实现交给了衍生类,自己只是抛出一个异常:

1
2
3
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

使用cancelAcquire方法是为了解决类似这样的bug:https://bugs.openjdk.java.net/browse/JDK-6503247

参考

Java中什么样的对象才能作为gc root:在cancelAcquire方法中,处于CANCEL状态的Node对象最终会被垃圾回收,这个帖子下面的回答介绍了哪些算gc root,适合不时review;