Java中断
2021-12-12 本文已影响0人
lz做过前端
线程状态转换图
概述
- Java的中断和操作系统的中断是两个层面的概念,
- 没有任何语言方面的需求一个被中断的线程应该终止。中断一个线程只是为了引起该线程的注意,被中断线程可以决定如何应对中断。
- 普遍的情况是,一个线程将把中断看作一个终止请求
中断是一种线程同步方式
- 如何使用
- 在一个线程中调用申明抛出 InterruptedException 的方法处捕获异常处理
- 在另一个线程中调用上面线程的 interrupt() 方法,即可触发阻塞的线程抛出 InterruptedException 进入异常处理
- 也可以在线程 run 方法循环获取中断标志去响应非阻塞中断,但这会让CPU很忙。并且循环里不能调用等待方法,因为一旦等待,程序将不能继续执行,直到通知或者被打断
- LockSupport.park 的打断
- LockSupport.park 可以被打断,打断后直接从 LockSupport.park 后开始执行
- 如果在线程没执行、未阻塞执行中、阻塞前调用中断,则线程在执行wait、sleep方法处不会阻塞
public class InterruptBeforeBlocking {
public static void main(String[] args) {
Object o = new Object();
Thread thread = new Thread(() -> {
synchronized (o) {
// for (int i = 0; i < 10000; i++) {
// System.out.println("for (int i = 0; i < 10000; i++)");
// }
// interrupted 会重置打断标志
// while (!Thread.interrupted()) {
// System.out.println("while interrupted is false");
// }
// 不会重置
while (!Thread.currentThread().isInterrupted()) {
System.out.println("while isInterrupted is false");
}
System.out.println("exist from loop");
try {
o.wait();
} catch (InterruptedException e) {
System.out.println("interrupted is " + Thread.interrupted());
// System.out.println("isInterrupted is " + Thread.currentThread().isInterrupted());
System.out.println("InterruptedException");
}
}
});
thread.start();
System.out.println("main interrupt begin");
thread.interrupt();
System.out.println("main interrupt end");
}
}
中断标志
- 中断标注位清除
- Thread.currentThread().isInterrupted() :它将线程中断标示位设置为true后,不会立刻清除中断标示位,即不会将中断标设置为false
- Thread.interrupted() :该方法调用后会将中断标示位清除,即重新设置为false
- Thread.currentThread().interrupt():可以重新设置当前线程中断标示,之后该线程中断标志为false
- 中断标注位清除意味着中断标志为false
Thread.interrupted() 与 thread.isInterrupted()
- Thread.interrupted() :测试当前线程是否已经中断。线程的中断状态 由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false。
- thread.isInterrupted():测试线程是否已经中断。线程的中断状态 不受该方法的影响。
thread.interrupt()
- thread.interrupt()方法不会中断一个正在运行的线程
- 这一方法实际上完成的是,设置thread线程的中断标示位,在线程受到阻塞的地方(如调用sleep、wait、join等地方)抛出一个异常InterruptedException,并且中断状态也将被清除,这样线程就得以退出阻塞的状态。
- 线程的中断标志位会由true重置为false
- 调用
object.wait(long timeout)
、LockSupport.park()
会让线程阻塞并将当前线程放置到对象的等待队列,线程的状态为waiting(thread.sleep(long millis)
不会释放锁,线程状态为timed_waiting,millis后线程自动苏醒进入就绪状态)。thread.interrupt()
、thread.notify()
后,waiting线程会进入同步队列竞争锁,如果获得锁,进入RUNNABLE状态,否则进入BLOCKED状态等待获取锁。
public class InterruptNeedReleaseLock {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
Thread thread = new Thread(() -> {
synchronized (o) {
// 这里导致锁不释放,主线程不会触发打断
// while (!Thread.currentThread().isInterrupted()) {
// System.out.println("while isInterrupted is false");
// }
System.out.println("exist from loop");
try {
o.wait();
} catch (InterruptedException e) {
// System.out.println("interrupted is " + Thread.interrupted());
System.out.println("isInterrupted is " + Thread.currentThread().isInterrupted());
// 如果不知道做什么设置为true,让外部Thread.currentThread().isInterrupted()获取打断状态
Thread.currentThread().interrupt();
System.out.println("isInterrupted is " + Thread.currentThread().isInterrupted());
System.out.println("InterruptedException");
}
}
});
thread.start();
// 让 thread 先拿到锁
Thread.sleep(3000);
synchronized (o) {
System.out.println("main interrupt begin");
thread.interrupt();
// 这里导致锁不被释放,即使thread被打算也不会执行异常后续的代码
// 所以打断也是先把thread移至等待队列,而只有锁被释放后,才会从等待队列移至同步队列
// 竞争获取到锁后才会执行异常后续的代码
for (; ; ) {
}
// System.out.println("main interrupt end");
}
}
}
中断I/O操作
- BIO
- BIO不可被中断,thread.interrupt()将不起作用,因为线程将不会退出被阻塞状态
- 比如ServerSocket的accept方法根本不抛出异常
- 但是,当调用该套接字的close方法时,如果发起thread.interrupt(),该线程在调用accept地方法将接收到一个SocketException(SocketException为IOException的子异常)异常
- NIO
- 实现InterruptibleChannel接口的通道是可中断的
- 另一个线程调用了该阻塞线程的 interrupt 方法,这将导致该通道被关闭,并且已阻塞线程接将会收到ClosedByInterruptException,并且设置已阻塞线程的中断状态
- 如果已设置某个线程的中断状态并且它在通道上调用某个阻塞的 I/O 操作,则该通道将关闭并且该线程立即接收到 ClosedByInterruptException
不可中断
- 不可中断的操作,包括进入synchronized段以及Lock.lock(),inputSteam.read()等,调用interrupt()对于这几个问题无效,因为它们都不抛出中断异常。如果拿不到资源,它们会无限期阻塞下去。
- 对于Lock.lock(),可以改用Lock.lockInterruptibly(),可被中断的加锁操作,它可以抛出中断异常。等同于等待时间无限长的Lock.tryLock(long time, TimeUnit unit)。
public class LockInterrupt {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread thread = new Thread(() -> {
System.out.println("thread run");
// 让主线程先执行打断
while (!Thread.currentThread().isInterrupted()) {
System.out.println("while isInterrupted is false");
}
System.out.println("thread will lock.lock");
lock.lock();
System.out.println("thread in lock.lock");
lock.unlock();
System.out.println("thread lock.lock out");
});
thread.start();
// 让thread先运行
Thread.sleep(5);
lock.lock();
System.out.println("main thread lock.lock in");
thread.interrupt();
// lock.unlock();
// System.out.println("main thread lock.lock out");
for (; ; ) {
//
}
// lock.unlock();
// System.out.println("main thread lock.lock out");
}
}
- 对于inputStream等资源,有些(实现了interruptibleChannel接口)可以通过close()方法将资源关闭,对应的阻塞也会被放开。
线程为何可以被中断,即中断的响应
回顾下之前讲的object.wait(long timeout)
、thread.sleep(long millis)
、LockSupport.park()
的JVM实现
- object.wait(long timeout) => ParkEvent -> park()
- thread.sleep(long millis) => SleepEvent -> park(millis)
- LockSupport.park() => UNSAFE.park(false, 0L) => ((JavaThread*)thread)->parker()->unpark();
即:线程上的ParkEvent、SleepEvent、parker对象锁
而thread.interrupt()方法的JVM实现,做了以下事情
- 将线程的打断标志设置为true
- 调用_SleepEvent.unpark() ,即唤醒thread.sleep(long millis)的阻塞
- 调用((JavaThread*)thread)->parker()->unpark(),即唤醒LockSupport.park()的阻塞
- 调用_ParkEvent.unpark(),即唤醒object.wait(long timeout)的阻塞
所以thread.interrupt()可以打断借由线程上的ParkEvent、SleepEvent、parker对象阻塞的线程,即object.wait(long timeout)
、thread.sleep(long millis)
、LockSupport.park()
,同时这些实现都申明在打断标志设置为true后会抛出InterruptException异常,所以这些方法会再打断执行以及竞争获取到锁后跳转到异常处理处继续执行