线程中断
中断不是类似 linux
里面的命令 kill -9 pid
,不是说我们中断某个线程,这个线程就停止运行了。中断代表线程状态,每个线程都关联了一个中断状态,是一个 true 或 false 的 boolean 值,初始值为 false。
关于中断状态,我们需要重点关注 Thread
类中的以下几个方法:
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}
// Thread 中的静态方法,检测调用这个方法的线程是否已经中断
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
// 所以,如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}
// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}
我们说 中断一个线程,其实就是设置了线程的 interrupted status
为 true
,至于说被中断的线程怎么处理这个状态,那是那个线程自己的事。如以下代码:
while (!Thread.interrupted()) {
doWork();
System.out.println("我做完一件事了,准备做下一件,如果没有其他线程中断我的话");
}
这种代码就是会响应中断的,它会在干活的时候先判断下中断状态,不过,除了 JDK 源码外,其他用中断的场景还是比较少的,毕竟 JDK 源码非常讲究。
当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。如果线程处于以下三种情况,那么当线程被中断的时候,能自动感知到:
-
来自
Object
类的wait()、wait(long)、wait(long, int)
,来自Thread
类的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)
这几个方法的相同之处是,方法上都有: throws InterruptedException 如果线程阻塞在这些方法上(我们知道,这些方法会让当前线程阻塞),这个时候如果其他线程对这个线程进行了中断,那么这个线程会从这些方法中立即返回,抛出 InterruptedException 异常,同时重置中断状态为 false。
-
实现了
InterruptibleChannel
接口的类中的一些 I/O 阻塞操作,如DatagramChannel
中的connect
方法和receive
方法等如果线程阻塞在这里,中断线程会导致这些方法抛出 ClosedByInterruptException 并重置中断状态。
-
Selector
中的select
方法一旦中断,方法立即返回
对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),并且在做出相应的操作后都会重置中断状态为 false。
那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj)
方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。
InterruptedException 概述
它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object
中的 wait()
方法,ReentrantLock
中的 lockInterruptibly()
方法,Thread
中的 sleep()
方法等等,这些方法都带有 throws InterruptedException
,我们通常称这些方法为阻塞方法(blocking method
)。
阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait
方法依赖于其他线程的 notify
,lock
方法依赖于其他线程的 unlock
等等。
当我们看到方法上带有 throws InterruptedException
时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。
除了几个特殊类(如 Object,Thread
等)外,感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候,就是通过在合适的时机(通常在循环的开始处)去判断线程的中断状态,然后做相应的操作(通常是方法直接返回或者抛出异常)。当然,我们也要看到,如果我们一次循环花的时间比较长的话,那么就需要比较长的时间才能感知到线程中断了。
处理中断
一旦中断发生,我们接收到了这个信息,然后怎么去处理中断呢?本小节将简单分析这个问题。我们经常会这么写代码:
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// ignore
}
// go on
当 sleep 结束继续往下执行的时候,我们往往都不知道这块代码是真的 sleep
了 10
秒,还是只休眠了 1
秒就被中断了。这个代码的问题在于,我们将这个异常信息吞掉了。(对于 sleep
方法,我相信大部分情况下,我们都不在意是否是中断了,这里是举例)
AQS 的做法很值得我们借鉴,我们知道 ReentrantLock
有两种 lock
方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
前面我们提到过,lock()
方法不响应中断。如果 thread1
调用了 lock()
方法,过了很久还没抢到锁,这个时候 thread2
对其进行了中断,thread1
是不响应这个请求的,它会继续抢锁,当然它不会把“被中断”这个信息扔掉。我们可以看以下代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 我们看到,这里也没做任何特殊处理,就是记录下来中断状态。
// 这样,如果外层方法需要去检测的时候,至少我们没有把这个信息丢了
selfInterrupt();// Thread.currentThread().interrupt();
}
而对于 lockInterruptibly()
方法,因为其方法上面有 throws InterruptedException
,这个信号告诉我们,如果我们要取消线程抢锁,直接中断这个线程即可,它会立即返回,抛出 InterruptedException
异常。
在并发包中,有非常多的这种处理中断的例子,提供两个方法,分别为响应中断和不响应中断,对于不响应中断的方法,记录中断而不是丢失这个信息。如 Condition
中的两个方法就是这样的:
void await() throws InterruptedException;
void awaitUninterruptibly();
实例分析
有以下代码:
synchronized (this) {
while (client == null) {
try {
this.wait();
} catch (InterruptedException e) {
LOGGER.error("InterruptedException:{}", e);
Thread.currentThread().interrupt();
}
}
}
上面的代码会造成什么问题?仔细分析可以发现,代码中如果抛出 InterruptedException
,就会陷入死循环中,导致异常日志打爆。为什么会这样呢?首先我们来看下这两个方法:
wait()
: if any thread interrupted the current thread before or while the current thread was waiting for a notification. The interrupted status of the current thread is cleared when this exception is thrown.Thread.currentThread().interrupt()
: If none of the previous conditions hold then this thread's interrupt status will be set.
wait()
在当前线程有中断标志位时抛出中断异常;而 interrupt()
如果当前线程没有在wait()
等阻塞操作,则标记中断。这样就陷入死循环,无限的打印 ERROR 日志。正确的处理 InterruptedException
是很重要的。