以前在写 C#
的时候,想要让线程暂停,就直接 Thread.Sleep(xxx)
就 OK
,但是在 Java
中,你得这样写:
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//或者
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
每 sleep
一次就得 try-catch
一次,实在太繁琐了。
相信大多人在最开始接触 InterruptedException
,对它的处理方式就是记录日志或则简单打印出来。然而,这是最错误的方法。
之所以会这么处理,是因为没有去了解 InterruptedException
的作用是什么,观察 JDK
中会抛出这个受检异常的大概有:
Threead#wait() Threead#sleep() Threead#join() BlockingQueue#take()/put() Lock#lockInteruptibly() AQS
他们的共同点就是阻塞,当执行这些方法后,线程状态就会由 RUNABLE
转变为 WATING
状态,然而在某些情况下,由于外界条件发生变化,比如用户觉得等太久了,不想再等了,那怎么将程序唤醒并接受这种阻塞状态呢?
答案就是 InterruptedException
Java中线程间协作及中断是通过 interrupt
协议
来实现的,当某个线程需要中断另外一个线程操作的时候,会将另外一个线程的 interrupt
变量置位 ture
, 同时会唤醒此线程
,至于是否响应中断,不同的方法有不同的处理,比如 synchronized
就会继续阻塞而不会响应中断。
一般常规的响应中断的方法,在检测到中断信号后,便会抛出 InterruptedException
异常,来通知其他方法该线程被中断,因此 InterruptedException
和其他异常不同的是,它算是一个信号,而不是真正的异常。
一般来说,想要将一个线程中断,需要调用以下方法
Thread#interrupt()
该方法具有两个作用:
interrupt
变量置位 true
因此,如果能够响应中断的方法,则需要不断的检测 interput
变量:
//JDK1.8 CountDownLatch#await()源码
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果线程被中断,则抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
可以看到,当 CountDownLatch
在阻塞前会检查中断变量,当需要中断的时候,就会直接抛出异常。
然而很多时候,需要中断的时候可能方法已经被阻塞了,因此 interrupt
还有一个作用便是 唤醒线程
//JDK1.8 AbstractQueuedSynchronizer#await()源码
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//省略部分代码
try {
for (;;) {
//省略部分代码
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,一般来说,需要响应中断的方法,都应该放在一个循环中,防止被因为中断而被唤醒,并且循环中应该包含检查中断的逻辑。
需要知道的是,一般其他方法在抛出 InterruptedException
之后,会将 interput
置为 false
,因此当接收到 InterruptedException
后,再调用 Thread#isInterrupt()
会返回 false
明白了 InterruptedException
的作用,我们就知道,接收到该异常后,直接打印到日志里面,是错误的做法。很多时候这并没有起到中断真正的作用。比如某些时候,需要将中断放在一个循环当中,那么你会发现中断无法被中断:
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
//第一段业务
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二段业务
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executorService.shutdownNow();
}
你会发现 shutdownNow()
只会让第一段业务结束,但是会马上卡在第二段业务代码中,也就是任务没有被成功取消。
查看源码你会发现, Executor#shutdownNow()/shutdown()
以及 FutureTask#cancel()
的任务取消都是基于中断实现的。
上面的代码之所以有问题就在于第一段业务逻辑直接将中断信号给“吞”了,导致第二个 sleep
并不知道线程已经中断。
进而我们可以知道,如果不知道具体的业务逻辑的话,可以在捕获到 InterruptedException
恢复中断变量。
public static void main(String[] args){
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
//第一段业务
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
//第二段业务
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
executorService.shutdownNow();
}
InterruptedException
是作为Java中用于线程间协作的中断