上一篇博客写了AQS加锁过程,文章后面只是浅谈了下阻塞线程被唤醒后该怎么执行。这篇博客主要描述解锁过程,以及再深入看下阻塞线程被唤醒后怎么走的。
解锁过程
从 reentrantLock.unlock()
方法走到 release(int arg)
。解锁的核心代码如下:
// AbstractQueuedSynchronizer.class
public final boolean release(int arg) {
// 是否解锁成功?
if (tryRelease(arg)) {
Node h = head;
// 此时头结点的 waitStatus 是 -1
if (h != null && h.waitStatus != 0)
// unpark 线程
unparkSuccessor(h);
// 解锁成功。
return true;
}
// 解锁失败。
return false;
}
前面的博客提到过,ReentrantLock 加锁就是将它里面的同步器的 state
字段改变(原本是0,改成大于0的数字),同样的,解锁就是将 state
字段改回原值。如果解锁成功了,ReentrantLock 就变成了自由的,允许再被别的线程加锁。但是,那些等待的线程现在还在 ReentrantLock 的同步器中阻塞着呢。所以接下来还要做的事情就是唤醒其中一个沉睡的线程。
所以,解锁过程分两步走:(1)解锁;(2)唤醒一个沉睡的线程。
接下来看下代码是怎么走的:
解锁
// ReentrantLock.class
// releases == 1
protected final boolean tryRelease(int releases) {
// 考虑到锁重入,解锁一次,state - 1,
int c = getState() - releases;
// 判断当前线程是不是所的持有者? 是的。
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是不是自由的。
boolean free = false;
// 当且仅当 state 的值被减到 0 时,解锁成功。
if (c == 0) {
free = true;
// 因为自由了,当前锁没有持有者。
setExclusiveOwnerThread(null);
}
// 设置 state == 0
setState(c);
return free;
}
这个过程还是很简单的,如果 state
能更新成 0,表示解锁成功,再将锁持有者置空就可以了。
唤醒一个沉睡的线程
唤醒哪一个沉睡的线程呢?因为公平锁先进先出,外加队头是空的,所以应该唤醒第二个节点中的线程。那如果第二个节点中的线程被取消了呢?
所以,唤醒的规则是:唤醒队列中离队头最近的并且没有被取消的线程。
基于这个思路,下面的代码就容易理解了。s
最终要指向那个满足唤醒条件的节点,它起初先指向第二个节点(node1),再判断 node1 是不是满足唤醒条件,如果满足,那直接到方法底部唤醒。如果不满足,令 s == null
,再通过 for 循环找到满足唤醒条件的节点。
// node == head
private void unparkSuccessor(Node node) {
// ws == -1
int ws = node.waitStatus;
if (ws < 0)
// 设置队头节点的 waitStatus == 0
compareAndSetWaitStatus(node, ws, 0);
// s 先指向队列中的第二个节点。
Node s = node.next;
// 如果第二个节点是空,或者被取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// t 从队尾开始,朝着队头的方向,扫一遍队列中的所有节点。
for (Node t = tail; t != null && t != node; t = t.prev)
// s 指向离队头最近的且没有被取消的线程节点。
if (t.waitStatus <= 0)
s = t;
}
// unpark s 指向的节点中的线程。
if (s != null)
LockSupport.unpark(s.thread);
}
两个小点
(1)if 中判断了s == null
,会有这种场景吗?
有的。比如队列中有 T1、T2、T3 线程,过段时间后,当T3执行完加锁代码并且解锁时,同步器队列中只剩了队头节点,s == head.next == null
。
(2)队列中被取消的线程节点怎么处理?
假设队列中有 node0(head)、node1、node2、node3 四个节点,node1 被取消了。当线程释放锁后,node2 中的线程会被 unpark,在释放锁阶段,仅此而已。不会对被取消的线程做任何操作。(后面会做的)
阻塞线程被唤醒后怎么办
上篇博客提高过,线程在哪里被 park 的,unpark 后就从那里继续执行。继续上面的那个场景,队列中有 node0(head)、node1、node2、node3 四个节点,node1 被取消了,node2 unpark。
T2 从 parkAndCheckInterrupt()
中开始执行,外围是个 for 死循环。
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()) // park
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
T2 循环了两次,看下具体细节:
第一次循环:
//【head,node1,node2,node3】
// node2 从 parkAndCheckInterrupt()中开始执行。
for (;;) {
// p == node1
final Node p = node.predecessor();
// p 不是 head,node2 拿锁失败。
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断要不要 park?
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
// pred == node1, node == node2
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// ws == 1
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
// 循环执行的结果是:队列变成了【head,node2,node3】
// 被取消的线程 T1 所在的节点 node1 被移出了队列。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 自旋,不 park。
return false;
}
第二次循环:
// 第二次循环
//【head,node2,node3】
for (;;) {
// p == head。
final Node p = node.predecessor();
// 尝试拿锁,而且会拿锁成功。
if (p == head && tryAcquire(arg)) {
// 设置 node2 为队头。
setHead(node);
// 准备删除原队头。
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
两个小点
(1)在加锁和解锁过程中,只在一个地方移出被取消的线程。那就是在shouldParkAfterFailedAcquire(Node pred, Node node)
方法中。
(2)被唤醒的线程尝试拿锁时,如果拿不到会自旋一次。上面的例子中分析的是因为 node1 是被取消的线程,所以自旋了一次。如果队列直接是【head,node2,node3】,node2 没有拿到锁,执行 shouldParkAfterFailedAcquire(head, node2)
, 因为 head 的 waitStatus
字段的值是 0,所以依旧还会再自旋一次。
与打断有关的小问题
(1)处于阻塞的线程节点被唤醒了,它会有拿不到锁的场景出现吗?既然依旧拿不到锁,为什么还要唤醒呢?
这个题目描述的不太准确,unpark 是“唤醒” park中的线程,此时被唤醒的线程是能拿到锁的。但是 park 的线程可能会被 interrupt()
打断,从而继续向下执行。被打断的线程开始尝试拿锁,(之前的线程还没释放锁)会失败。
(2)如果最终结果是 park,被唤醒的线程会自旋一次,被打断的线程会自旋几次?
可能会自旋0次,1次。
0次:
队列【head,node1,node2,node3】,队列中节点都处在正常的 park 状态,加锁代码块正在被执行着,node2 被打断。node2 尝试拿锁失败,又因为node1的waitStatus
字段是 -1,所以判定 node2 直接进入 park。
1次:
队列【head,node1,node2,node3】,node1 被取消了,加锁代码块正在被执行着,node2 被打断。node2 尝试拿锁失败,因为 node1 的 waitStatus == 1
,因为清理 node1 ,允许 node2 自旋一次,再尝试拿锁,第二次拿锁失败,判定要 park。
(3)如果被打断的线程拿到锁了,怎么办?
它会自己打断自己,把锁让出来。