一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。基于这种思想,Thread.stop()、Thread.suspend()、Thread.resume()这些暴力的线程操作都已经被废弃了。
Java并发编程中就没有终止线程的手段了吗?当然不是。取而代之的是优雅的关闭方式:Thread.interrupt()。有一点需要谨记,interrupt() 并不会真正的中断线程,而是修改一个标记位。
在线程A中调用线程B的 interrupt(),并不会立刻中断线程B,而是通知线程B:你应该中断了!至于线程B 到底是否中断,是由它自己控制的(可以是检测到线程处于中断状态,抛出 InterruptedException;也可以继续运行线程)。
如果一个线程被阻塞了,调用interrupt()会使得当前线程立即退出阻塞。阻塞方式有:Object.wait()、Thread.sleep()、 Thread.join()、LockSupport.park()等。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(DateTime.now().toString());
LockSupport.park();
System.out.println(DateTime.now().toString());
System.out.println(Thread.interrupted());
System.out.println(Thread.interrupted());
});
thread.join();
thread.start();
// 保证线程启动成功并且已经阻塞
Thread.sleep(3000);
// 通知中断
thread.interrupt();
}
// 结果输出
2021-04-23T15:29:53.969+08:00
2021-04-23T15:29:56.950+08:00 // 前后时间相差3秒,说明通知中断后,线程立即退出了阻塞
true
false
复制代码
但是,大家有没有发现:前后两次线程中断状态的输出结果不一致?让我们深入源码一探究竟。
// 测试当前线程是否已中断
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 中断状态是否重置取决于传递的 ClearInterrupted值。
private native boolean isInterrupted(boolean ClearInterrupted);
复制代码
注意看这个 boolean类型的参数 ClearInterrupted,已经够见名知意了吧!
interrupt()并不是真正意义上的中断线程,只是修改了其内部的一个状态值。
前置知识了解完毕,接下来让我们进入正题!
示例代码:
public static void main(String[] args) throws InterruptedException {
final ReentrantLock lock = new ReentrantLock();
// 线程运行体
Runnable runnable = () -> {
try {
lock.lockInterruptibly();
System.out.println(Thread.currentThread().getName() + " 获取锁,执行业务逻辑!");
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
// 创建两个线程
Thread thread1 = new Thread(runnable, "thread-1");
Thread thread2 = new Thread(runnable, "thread-2");
thread1.start();
// 保证thread-2后于thread-1启动
Thread.sleep(500);
thread2.start();
// 睡眠一秒,通知 thread2中断
Thread.sleep(1000);
thread2.interrupt();
}
复制代码
运行代码,控制台输出:
thread-1 获取锁,执行业务逻辑!
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.edu.ReentrantLockTestInterrupt.lambda$main$0(ReentrantLockTestInterrupt.java:18)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "thread-2" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.edu.ReentrantLockTestInterrupt.lambda$main$0(ReentrantLockTestInterrupt.java:25)
at java.lang.Thread.run(Thread.java:748)
复制代码
让我们了解代码运行过程中,都发生了什么事:
- thread1 开始运行,获取到锁,接下来的3秒内都是处于睡眠状态(模拟业务场景);
- thread1 运行0.5秒后,thread2开始运行,由于锁被thread1持有,获取失败,进入等待队列,阻塞;
- thread1 运行1.5秒后,通知thread2中断线程,thread2会立刻退出阻塞状态。由于使用的是中断锁,thread2响应中断信号,抛出 InterruptedException;执行finally块中的解锁逻辑,由于 thread2没有获取到锁,非独占线程,抛出 IllegalMonitorStateException。
前情提要:ReentranLock 加锁和解锁过程,在上一篇博客中有详细介绍。
# java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg) throws InterruptedException {
// 将当前线程包装成一个独占类型的node节点,添加到等候队列的尾部
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;
}
// 公平锁中,不是抛出异常,而是局部变量 interrupted设为 true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
如果读者老爷细心的看了上一篇博客,就会发现中断模式下进入阻塞队列的逻辑和(非)公平锁基本上相同,唯一不同的是这里检测到线程中断后抛出 InterruptedException异常,公平锁是返回一个是否中断的boolean值。
有个点这里需要说明一下,公平锁 的请求锁方法 acquire()中有个 selfInterrupt();的逻辑。字面意思就能看出来:自我中断。为什么会有这么一段逻辑呢?
友情提示:线程自我检测中断状态后,会清除该状态值!
公平锁模式下,线程A 还在等候队列中阻塞着,线程B 调用了线程A的 interrupt(),线程A 会立刻退出阻塞状态,修改线程局部变量 interrupted = true。由于线程A 可能并不是候选者线程,所以一次循环过后,继续进入阻塞状态。直到当前线程成为候选者,被唤醒并获取到锁,返回 interrupted变量(true)。执行 selfInterrupt(),将清除掉的状态重复设置回来。如果后续的操作(比如同步块)中,有中断检测抛异常,即响应了线程B的中断请求。
thread2 响应了中断请求后,执行 throw new InterruptedException(),跳出死循环。由于 failed值是true,执行取消获取锁逻辑 cancelAcquire(node)。
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 解除 取消状态的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// pred 是node头节点方向中最近的正常的前驱节点。
Node predNext = pred.next;
// 使用无条件写入代替CAS。在这个原子步骤之后,其他node可以跳过我们。
// waitStatus是被 volatile修饰的
node.waitStatus = Node.CANCELLED;
// 如果node是尾节点的话,将它的前驱节点设为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
// 将设置成功的尾节点的后驱节点置为null help Gc
compareAndSetNext(pred, predNext, null);
} else {
// node 不是尾节点的情况下
// 如果 pred是头节点、或者非通知状态(CAS失败)、或者不持有线程,则唤醒node的后驱节点
// 否则,CAS设置 pred的后驱节点为 next
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
}
}
复制代码