AQS源码分析——一步一步带你走进AQS的世界(二)
源码分析AQS
上一篇文章,以ReentrantLock的角度去分析了AQS的加锁和解锁,一行一行逐行分析,相信都能看懂,接下来我们要进入一个新的大关,就是Condition。
前文回顾
前面简单的介绍了一下阻塞队列,阻塞队列要记住的几个要点:
- 阻塞队列是一个双向链表
- 阻塞队列的头节点,head节点,不在阻塞队列中,head节点往往是拿到了锁的线程
- 阻塞队列里的节点被封装成了Node类
面试题
这里就分享一道本人面试阿里的时候出现的面试题:
为什么AQS唤醒线程的时候为什么从后向前遍历?这道题在上一篇文章已经解释过了,这里再提一下。
首先我们需要看看唤醒的这部分代码,如下所示:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 这里是查看当前节点的后序节点 s 如果s为null || s的状态失效 (状态值 > 0 可以理解为取消排队)
// 这个时候就从尾部往前遍历 找到一个处于正常阻塞状态的节点进行唤醒
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);//唤醒线程
}
可以看到在for循环中,是从后往前遍历,直到找到当前节点为止。
接下来我们来看节点的入队逻辑,为什么需要从后往前遍历,就是因为需要解决在高并发下入队可能会存在的问题,代码如下所示:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 初始化头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 注意这一步没有使用CAS 注意这一步没有使用CAS 注意这一步没有使用CAS
node.prev = t;
// 这一步使用 CAS 将当前节点替换为尾节点
if (compareAndSetTail(t, node)) {
//前驱节点的next指针指向当前节点
t.next = node;
return t;
}
}
}
}
可以看到在上述代码中,AQS使用了CAS来保证将当前节点置为尾节点的线程安全,但是在CAS之前的上一步,没有使用任何手段来保证线程安全,直接将当前节点的前序指针指向了尾节点。
那么可以想象到,在高并发的情况下,会出现什么问题?
线程A通过CAS操作将当前节点置为了尾节点,这个时候线程A发生上下文切换,线程B再次使用CAS将当前节点置为尾节点,同时将这个if代码块中的内容全部执行完毕,那么这个情况下,整个流程如下图所示:
看到上面的两个图,就很清楚为什么需要需要从后往前遍历了。
Condition
首先Condition,翻译出来就是条件,所以这里要讲到AQS中的条件队列了,首先看看Condition的使用场景以及使用示例,Condition就是经典的被用来解决生产者/消费者问题的存在。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依赖于 lock 来产生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生产
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
// 队列已满 等待 直到 not full 才能继续生产
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
// 生产成功 队列已经 not empty 了 发个通知出去
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 消费
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
// 队列为空 等待 直到队列 not empty 才能继续消费
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
// 消费数据 队列 not full 了 发个通知
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
上面这个例子是Doug Lea给出的例子,很好的诠释了Condition的使用场景是什么,上面的例子很简单,下面来进行一个总结:
- 在使用Condition时,可以看到必须要依赖于锁lock去产生Condition,也就是说,一个锁lock,对应一个条件队列(条件队列是单向链表,不是双向链表,请牢记)在使用时必须要先持有相应的锁。
- ArrayBlockingQueue采用的就是这样的方式实现了生产者消费者,实际生产中可以直接使用它,它内部持有ReentrantLock,相关源码如下所示:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/**
* Serialization ID. This class relies on default serialization
* even for the items array, which is default-serialized, even if
* it is empty. Otherwise it could not be declared final, which is
* necessary here.
*/
private static final long serialVersionUID = -817911632652898426L;
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
int count;
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;
......
首先,我们需要直到Condition与Reentrant和AQS的联系,需要捋清楚才能继续。
通过上图我们可以看出,AQS内部持有一个ConditionObject实现了Condition接口,而ReentrantLock是通过Sync来管理锁的,所以ReentrantLock通过Sync的newCondition方法去返回一个ConditionObject。
上面说过,使用Condition必须要获取相应的锁,而一个锁可以创建出多个Condition,这个看Sync的newCondition()方法就可以看出来了:
final ConditionObject newCondition() {
return new ConditionObject();
}
接下来我们看AQS内部的ConditionObject类的源码,首先我们需要看看它的属性:
public class ConditionObject implements Condition, java.io.Serializable {
// 序列化用
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
// 头节点
private transient Node firstWaiter;
/** Last node of condition queue. */
// 尾节点
private transient Node lastWaiter;
......
那么为什么说它是单向链表呢,主要原因在于Node类的源码中有一个属性为nextWaiter,如下所示:
// 前序指针——用于阻塞队列
volatile Node prev;
// 后序指针——用于阻塞队列
volatile Node next;
// 后序指针——用于条件队列
Node nextWaiter;
在上一篇中,AQS有一个阻塞队列来保存等待获取锁的线程,这里引入另一个新的概念,就是条件队列,如下图所示:
上面这张图就能很好的诠释了Condition的处理流程了:
- 阻塞队列中,线程被封装成Node,在条件队列中,线程依然是Node,因为条件队列的节点迟早是要转移到阻塞队列中去的。
- 一个ReentrantLock可以创造多个条件队列,比如上图中的condition1、condition2。
- 线程A调用condition1.await()方法,那么线程A会被封装成Node,加入condition1条件队列中,阻塞在这里,不继续往下执行,直到被唤醒。
- 调用condition1.signal()方法,触发一次唤醒,唤醒的是对头,也就是firstWaiter节点,会将条件队列中的头节点转移到阻塞队列的队尾,等待获取锁,获取锁之后,才能在await方法返回,继续往下执行。
这里需要记住,阻塞队列和条件队列都是Node节点,并且条件队列的Node节点会被转移到阻塞队列中,等待获取锁,获取锁后才能在await方法处返回。
接下来走代码分析:
// 看到抛出中断异常就应该意识到这个方法是可以中断的
public final void await() throws InterruptedException {
// 判断中断状态
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程封装成Node节点加入到Condition的条件队列中
Node node = addConditionWaiter();
// 这一步就是释放锁 并且记录释放锁之前Node的状态值
// 因为如果它后续加入了阻塞队列并且拿到了锁 肯定是需要恢复到之前拿到锁的状态
// 比如 可重入锁 重入了2次 那么恢复它的时候 就应该拿到2次锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 看到循环 应该分析推出循环的条件了
// 1 当isOnSyncQueue(node) 返回true的时候 也就是 node已经被转移到阻塞队列了
// 这里的设计十分巧妙 到下面我们看到源码就知道了
// 2 当发生线程中断的时候 会从循环break出来
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 能运行到这里 要么被唤醒 要么被中断
// 唤醒之后的流程就是 加入到阻塞队列中 竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
接下来我们来详细分析其中的方法。
addConditionWaiter()方法——将节点加入到条件队列中
private Node addConditionWaiter() {
// t 记录尾节点
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果尾节点的状态值是CONDITION(-2) 就将其清除出去
// 注意这里只要状态值 != -2就被认定为是取消排队 原因在下面就可以看到
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍历整个条件队列 将取消排队的节点全部都清除出去
unlinkCancelledWaiters();
t = lastWaiter;
}
// 将当前线程包装成Node节点
// 这里需要注意 在初始化的时候就已经将节点的状态值置为了CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果尾节点为空 则直接将node放在头节点的位置上
if (t == null)
firstWaiter = node;
else
// 否则就让尾节点的后序指针指向node 这是node就成为了最新的尾节点
t.nextWaiter = node;
// 将尾节点指针更新为node
lastWaiter = node;
return node;
}
上面的代码就是将当前线程封装成Node,再把Node加入到条件队列中的队尾。
接下来分析unlinkCancelledWaiters()方法,这个方法就是遍历整个条件队列,清除条件队列中取消等待的节点,这个方法被调用的时机就是:
- 在await方法时,如果发生了取消操作
- 在节点入队的时候,发现尾节点的状态是被取消的
都会调用这个方法。
private void unlinkCancelledWaiters() {
// 记录头节点
Node t = firstWaiter;
Node trail = null;
// 向后遍历
while (t != null) {
Node next = t.nextWaiter;
// 只要节点的状态值不是CONDITION就被认为是取消排队的
if (t.waitStatus != Node.CONDITION) {
// 链表操作 将不符合条件的链表剔除出去
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
fullyRelease(node)方法——完全释放独占锁
回到await方法,上一步是将节点加入到了条件队列中,下一步就是完全释放锁,并且记录锁的state值,这一部分上面已经解释过了,因为ReentrantLock是可重入的,所以在下次在被唤醒的时候,应该要恢复到之前的状态才可以继续,比如它之前重入了2次锁,那么state值就应该为2,那么唤醒它的时候,就也需要获取两次锁才能继续执行。
// 这个方法如果失败 会将节点设置为取消状态 并且抛出IllegalMonitorStateException异常
final int fullyRelease(Node node) {
// 成功 / 失败 标识
boolean failed = true;
try {
// 记录当前的state值
int savedState = getState();
// 这里就是完全释放锁 将state置为0
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
看到这里,就能知道为什么一定要先持有锁,才能调用await方法,如果一个线程A没有持有锁就直接调用了await方法,那么在这个方法中的release(savedState)处一定会抛出异常,然后将这个节点的状态值设置为取消排队,这样就会被后续的节点清除掉。
等待进入阻塞队列
int interruptMode = 0;
// 循环调用挂起方法 防止被虚假唤醒
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
// 判断线程是否被中断了
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue()方法用于判断当前节点是否已经被转移到阻塞队列中了:
final boolean isOnSyncQueue(Node node) {
// 这里就是判断节点的状态值是否是等待状态 | 节点的prev指向是否是null
// 这里的prev这里需要这么去理解 prev是阻塞队列中使用的 如果它的prev指向的是null 说明肯定没有在阻塞队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果node有后序节点 next的时候一定是在阻塞队列中的
if (node.next != null) // If has successor, it must be on queue
return true;
// 这个方法就是从阻塞队列的队尾开始寻找 从后往前遍历 如果找到相等的说明在阻塞队列中
// 如果没有找到 说明还在条件队列中
return findNodeFromTail(node);
}
// 从阻塞队列的队尾 往前遍历 如果找到 则返回true
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
回到前面的循环中,如果isOnSyncQueue方法返回false,则需要调用LockSupport.park(this)方法将线程挂起。
signal唤醒线程,将Node转移到阻塞队列中
// 唤醒条件队列中的头节点——也就是等待了最久的节点(当然也有可能唤醒失败 因为可能头节点取消等待)
// 将这个线程对应的 node 从条件队列转移到阻塞队列
public final void signal() {
// 调用 signal 方法的线程必须持有当前的独占锁 否则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 从条件队列队头往后遍历,找出第一个需要转移的 node
// 因为前面我们说过 有些线程会取消排队 但是可能还在队列中
private void doSignal(Node first) {
do {
// 将firstWaiter指针指向first的下一个节点
// 如果firstWaiter 为 null 的话就说明条件队列为空了 将尾节点置为null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 因为 first 要被移到阻塞队列了 和条件队列的链接关系在这里断掉
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
// 这里 while 循环 如果 first 转移不成功(节点取消排队) 那么选择 first 后面的第一个节点进行转移 依此类推
}
// 将节点从条件队列转移到阻塞队列
// true 代表成功转移
// false 代表在 signal 之前 节点已经取消了
final boolean transferForSignal(Node node) {
// CAS 如果失败 说明此 node 的 waitStatus 已不是 Node.CONDITION 说明节点已经取消
// 既然已经取消 也就不需要转移了 方法返回 转移后面一个节点
// 否则 将 waitStatus 置为 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// enq(node): 自旋进入阻塞队列的队尾
// 注意 这里的返回值 p 是 node 在阻塞队列的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 说明 node 在阻塞队列中的前驱节点取消了等待锁 直接唤醒 node 对应的线程
// 唤醒node节点之后会发生什么 后面会说明
// 如果 ws <= 0, 那么 compareAndSetWaitStatus 将会被调用 上篇介绍的时候说过
// 节点入队后 需要把前驱节点的状态设为 Node.SIGNAL(-1) 表明当前节点的后序节点需要被唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驱节点取消排队 或者 CAS 失败 会进到这里唤醒线程 之后的操作看下一节
LockSupport.unpark(node.thread);
return true;
}
正常情况下ws<=0并且CAS操作会返回true,所以一般不会进入if语句唤醒节点,然后返回true,表明这个节点已经成功加入阻塞队列中。假如出现了特殊情况,阻塞队列中的前序节点的状态值大于0(ws > 0)或者CAS失败,就需要唤醒线程,让其进行到下一步。
唤醒后检查中断状态
在上一步signal唤醒线程之后,将Node转移到阻塞队列后就等待获取锁,只要重新获取到了锁就可以继续往下执行,就走到了如下代码所示:
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 线程挂起 -- 从这里醒来
LockSupport.park(this);
// 检查中断状态
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
首先我们需要看到interruptMode变量,这个变量可以取的值有0、THROW_IE(-1)、REINTERRUPT(1),这个取值可以在await方法中可以看到有这样的三个取值。
- 0:初始化的时候的值,说明在await期间没有发生中断
- THROW_IE(-1):代表 await 返回的时候,需要抛出 InterruptedException 异常
- REINTERRUPT(1):代表 await 返回的时候,需要重新设置中断状态
有四种情况会LockSupport.park(this)这句话继续往下执行。
- signal后转移节点到阻塞队列中并且成功获取锁
- 线程中断,在park的时候,另一个线程对这个线程进行了中断
- signal的时候,转移节点的前序节点取消了,或者改变前序节点状态值的CAS操作失败了
- 虚假假唤醒,Object.wait()也会存在这样的问题,所以一般都是在循环挂起线程
线程被唤醒之后,第一步就是检查线程是否在挂起期间发生中断,如果发生中断,需要判断是在signal之前发生的中断还是在signal之后发生的中断,然后赋值给interruptMode。
- 在signal之前发生的中断,返回THROW_IE (-1)
- 在signal之后发生的中断,返回REINTERRUPT (1)
- 没有发生中断,返回0
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
Thread.interrupted():如果线程已经发生中断了,返回true并且将中断状态重置为false,所以才有后序的重新中断(REINTERRUPT)的使用。
看看是如何判断是在signal之前发生的中断还是在signal之后发生的中断:
// 能进入到这个方法 一定是线程处于中断状态 否则直接返回0 不会进入到该方法
final boolean transferAfterCancelledWait(Node node) {
// 用 CAS 将节点状态设置为 0
// 如果这步 CAS 成功 说明是 signal 方法之前发生的中断
// 因为如果 signal 先发生的话 signal过程中会将 waitStatus 设置为 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 将节点放入阻塞队列 记住 即使中断发生了 依然会放入阻塞队列中
enq(node);
return true;
}
// 到这里是因为 CAS 失败 肯定是因为 signal 方法已经将 waitStatus 设置为了 0
// signal 方法将节点转移到阻塞队列 但是可能没有完成转移 下面就是在等待它 完成转移
// 这种情况就是:signal 调用之后 没完成转移之前 发生了中断
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
这里需要记住,即使发生了中断,还是会将节点转移到阻塞队列中去,所以上述方法中的while循环,要么节点完成转移返回,要么发生中断返回。
这里说的就是,假如线程A在条件队列中,此时它被中断了,中断了就会被唤醒,它被唤醒之后发现自己不是被signal的那个线程,此时它依然会主动进入阻塞队列中去。
获取独占锁
在await方法中的循环退出之后,进入的就是如下代码中:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
退出while循环,就说明节点一定进入了阻塞队列中,准备获取锁,所以这里就是将线程还原回释放锁之前的状态,也就是将state值变为之前的saveState值,acquireQueued(node, savedState)这个方法返回的时候,就是代表线程获取了锁,并且state=saveState。
注意,不管是否发生中断,节点都会进入到阻塞队列中,acquireQueued(node, savedState) 的返回值就是代表线程是否被中断。如果返回true,表示发生中断,此时再判断interruptMode != THROW_IE,说明在signal之前就已经发生中断了,那么就需要将interruptMode值置为REINTERRUPT,用于待会儿重新中断。
继续往下走代码:
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
这里说说node.nextWaiter != null怎么满足,在signal的时候会将节点转移到阻塞队列中,并且有一步就是node.nextWaiter = null,将节点和条件队列的联系断开,但是如果在signal之前发生中断,此时节点也会进入阻塞队列中,并且此时没有将节点与条件队列的联系断开,也就是没有执行node.nextWaiter = null这一步。
如果node.nextWaiter != null,那么就会清除条件队列中取消排队的节点,全部清除出去,这个方法在上面已经讲过了,接下来就会进入到reportInterruptAfterWait方法,处理中断状态。
处理中断状态
这里就又要提到interruptMode这个值了,稍微回顾一下:
- 0:表示没有被中断过
- THROW_IE:await方法抛出中断异常,代表在await期间发生了中断
- REINTERRUPT:重新中断线程,它表示await期间没有发生中断,而是在signal之后发生的中断
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// 如果interruptMode为THROW_IE,表示在await期间发生中断,直接抛出中断异常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 如果interruptMode为REINTERRUPT,表示线程需要再次中断
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
static void selfInterrupt() {
// 当前线程中断
Thread.currentThread().interrupt();
}
带超时机制的await方法
带超时机制的await很简单,之前是调用park方法来挂起线程,等待被唤醒,而现在是调用parkNanos方法,来指定休眠的时间,醒来之后再判断signal是否被调用,调用了就是没有超时,否则就是超时了,超时的话,自己来进行转移到阻塞队列中,然后进行正常流程的抢锁。
AQS独占锁的取消排队
接下来就应该说说AQS如何取消排队,我们需要在acquireQueued这个方法中找答案:
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);
}
}
这个方法在上一篇文章中已经是介绍过了,能进入到这个方法的一定是节点已经存在于阻塞队列中的,下面看到parkAndCheckInterrupt方法:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
把这两段代码连起来看,会稍微清楚一点,首先,如果我们需要取消一个线程的排队,就需要另一个线程去中断它,比如线程A调用lock方法后很久不返回,此时线程B中断线程A,线程A在LockSupply.park(this)方法中被唤醒,然后执行Thread.interrupted();返回true。
这里需要注意,中断线程不等于结束运行,这里只是简单的记录了一下它发生中断过,然后继续下一次循环抢锁,并且由于Thread.interrupted()方法会清除中断标志,下一次进来就是返回false。并且这里方法的出口就只有一个,就是返回interrupted值,如果发生中断,这个值就会变成true,然后在抢到锁时返回true。
这个时候需要联系acquireQueued的外层方法,来看看外层方法是怎么处理acquireQueued方法返回true的。(返回true,说明线程发生中断)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
这里看到很简单,acquireQueued方法返回true,那么就会设置线程中断状态,但是不抛出任何异常。
这里的逻辑就是,lock方法处理中断,即便是发生了中断也没关系,我只是记录一下发生过中断,我还是会进行抢锁,抢到锁之后,就设置一下线程的中断状态,但是也不抛出任何异常,调用者在获取锁之后,可以去判断是否发生过中断,也可以不去管它。
目前和取消排队还没有什么联系,但是接下来可以看到另一个的lock方法:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
方法上多了一个抛出中断异常的方法,继续看里面的代码:
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
继续往里看doAcquireInterruptibly:
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())
// 可以看到这里和上面的lock方法不一样
// 这里不再是标记中断标识 然后继续下一次循环了 而是直接抛出异常
// 一直往外抛出
throw new InterruptedException();
}
} finally {
// 如果通过 InterruptedException 异常出去,那么 failed 就是 true 了
if (failed)
cancelAcquire(node);
}
}
这里需要注意的就是要理解一点,中断不等于退出程序,中断在线程中只是一个中断标识,是一个true or false,初始值为false,后面会写一篇关于中断的详细解释。
转载自:https://juejin.cn/post/7227012644507451450