上一章仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:
AQS 中阻塞的线程被唤醒后的执行流程 (本篇讲述)
可打断的锁 lock.lockInterruptibly()
锁超时 lock.tryLock(long,TimeUnit)
条件变量 Condition
线程尝试获取锁,不论公平锁还是非公平锁,如果获取不到,最后都会进入到 AQS 的队列中进行阻塞等待。直到持有锁的线程将锁释放,通过从后往前遍历查找 AQS 队列中距离 head 最近的可用节点,将其线程唤醒:LockSupport.unpark( s.thread)
。 唤醒后的线程将会继续尝试竞争锁,我们分析一下它是如何竞争的:
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 GCfailed = 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. 解锁前:(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)) {//争锁成功,成为新的 空headsetHead(node);//将原先的空head抛弃p.next = null; // help GCfailed = false;return interrupted;}...
}private void setHead(Node node) {//成为新的headhead = node;//将封装在 node 中的 thread 解放出去node.thread = null;//清空prev指向node.prev = null;
}
而竞争失败的 thread3 将会如上一篇所言,进入 AQS 等待队列,并通过 LockSupport.park() 进入 等待状态(WAITING)。