ReentrantLock源码解析(补充1)
上一章仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:
-
AQS 中阻塞的线程被唤醒后的执行流程 (本篇讲述)
-
可打断的锁
lock.lockInterruptibly()
-
锁超时
lock.tryLock(long,TimeUnit)
-
条件变量 Condition
1. AQS 中阻塞的线程被唤醒后的执行流程
线程尝试获取锁,不论公平锁还是非公平锁,如果获取不到,最后都会进入到 AQS 的队列中进行阻塞等待。直到持有锁的线程将锁释放,通过从后往前遍历查找 AQS 队列中距离 head 最近的可用节点,将其线程唤醒:LockSupport.unpark( s.thread)
。 唤醒后的线程将会继续尝试竞争锁,我们分析一下它是如何竞争的:
1.1 线程被唤醒后,会继续执行 LockSupport.park() 之后的代码
LockSupport.park(thread) 方法会让Java线程进入 等待状态(WAITING),Java线程状态详情参见:
Java-线程基础
LockSupport.unpark(thread)调用之后,线程被唤醒,并获取到 CPU 时间片后,将继续运行 LockSupport.park() 位置后续的代码。(thread.interrupt()也可以将在 等待状态 线程唤醒)
在 ReentrantLock 中,AQS 中阻塞等待的线程被唤醒后,将继续下述代码的内容:
//AbstractQueuedSynchronized.java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果当前节点的前驱为head(这是一个空节点,标志着 AQS 双向链表中的头),那么可以尝试获取锁
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//之前被阻塞在 parkAndCheckInterrupt() 方法中
//shouldParkAfterFailedAcquire()方法将node的前驱节点中废弃节点(waitStatus = CANCELLED = 1)全都清理出队列。
//1. 如果有废弃节点要清理,那么该方法return false,视图再次进入循环判断当前线程所在节点是否处在队列的队头位置。
//2. 如果没有废弃节点要清理,那么说明当前线程不处在队头,那么就要进入 等待状态(WAITING) 阻塞。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
//之前通过LockSupport.park()方法进入阻塞状态
//LockSupport.unpark()或者interrupt()之后,会继续代码执行
LockSupport.park(this);
//Thread.interrupted()将会返回线程的打断标记,并且清空打断标记。
return Thread.interrupted();
}
由于 LockSupport.park() 而处在 等待状态(WAITING) 的线程不仅可以通过 LockSupport.unpark() 方法唤醒,也可以通过 thread.interrupt() 方法唤醒,区别是后者将会让 thread 的打断标记置为 true。
线程唤醒后,继续执行 parkAndCheckInterrupt() 的 return 部分代码。而后进入到acquireQueued() 中继续死循环,尝试获取锁,或者重新回到 AQS 的等待队列中阻塞。
1.2 线程唤醒后的竞争分析
1. 解锁前:(node为双向指针,我画漏了)
假设thread1所在node之前所有的废弃节点(waitStatus = CANCELLED = 1)以及全部清空,当前 node(thread1) 已经处在队列头。
被唤醒时,进入 acquireQueued() 的 for(;😉 循环,由于 node(thread1) 为队头,由 acquireQueued() 代码可知,该线程将进行 tryAcquire() 尝试获取锁资源。
final boolean acquireQueued(final Node node, int arg) {
...
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
...
}
...
}
如果没有出现竞争,如上一章讨论的上锁流程, node(thread1) 将会成为新的空head,thread1 从 node 中脱离出来,继续执行后续临界区代码。
本篇中,我们补充讨论出现竞争的情况:
2. 解锁后,出现竞争:(node为双向指针,我画漏了)
node(thread1) 被唤醒后,由于处在空head之后,为首个可用队头节点,将进入到 tryAcquire() 尝试获取所资源,同时 thread3 也在尝试获取锁资源。
3. 竞争失败 (node为双向指针,我画漏了)
如果 node(thread1) 竞争失败,将会进入 acquireQueue() 中下列部分,再次通过 parkAndCheckInterrupt() 的 LockSupport.lock() 方法阻塞起来。需要注意的是,这里没有额外动作,node(thread1) 在队列中的位置不变:
final boolean acquireQueued(final Node node, int arg) {
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
...
}
// tryAquire()竞争失败 return false后,进入下面部分
if (shouldParkAfterFailedAcquire(p, node) &&
//再次阻塞起来
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
4.竞争成功
竞争成功,thread1将会从node中解放出来,进入临界区运行后续代码:
Thread thread1 = new Thread(()->{
lock.lock();
//获取到锁之后,进入下面的临界区代码
try{
//临界区代码
}finally{
lock.unlock();
}
});
而 node 将会被置空,并成为新的 空head 节点,原先的 空head 节点被抛弃:
final boolean acquireQueued(final Node node, int arg) {
...
if (p == head && tryAcquire(arg)) {
//争锁成功,成为新的 空head
setHead(node);
//将原先的空head抛弃
p.next = null; // help GC
failed = false;
return interrupted;
}
...
}
private void setHead(Node node) {
//成为新的head
head = node;
//将封装在 node 中的 thread 解放出去
node.thread = null;
//清空prev指向
node.prev = null;
}
而竞争失败的 thread3 将会如上一篇所言,进入 AQS 等待队列,并通过 LockSupport.park() 进入 等待状态(WAITING)。