Java并发编程笔记(七):取消和关闭
任务和线程的启动时非常容易的,但是安全地取消和关闭并不容易,大多数时候,任务执行完毕就会自然的关闭线程了。也有有一些任务可能会永远执行亦或者任务执行时间太长,用户希望关闭它,但如果强行关闭,可能会导致“不一致”的情况,故安全,可靠的关闭任务是一个非常重要的步骤。
取消某个任务的原因有很多:
- 用户请求取消。例如在搜索进度条还没达到100%的时候,用户不耐烦了,点击取消按钮。
- 任务超时。有些任务是对时间敏感的。任务超时的时候必须停止且保存状态。
- 应用程序事件。例如多线程搜索,一个线程已经完成了任务,其他线程应当被取消。
- 错误。发生错误的时候也应该关闭线程,取消任务。
.....
一、使用标志(flag)来标识关闭状态
这是最简单的方式了,直接根据一个状态值来判断是否需要关闭。如下代码所示:
public void generate() {
BigInteger temp = BigInteger.ONE;
while (!isCancel) {
temp = temp.nextProbablePrime();
try {
synchronized (this) {
bigIntegers.put(temp);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("end");
}
这里使用isCancel做状态标志,可以通过设置这个标志位来使得停止任务。但如果任务是阻塞的,例如将素数放入BlockingQueue里,那么因为put操作是阻塞的,如果生产者生成素数的速率比消费者大,BlockingQueue总会有被填满的时候,此时put操作就会被阻塞,会导致无法检查isCancel的状态,最终无法停止程序。这种情况可以使用中断。
二、中断
Java里的中断和计算机底层的中断机制不太一样。Java里的中断不会强制被中断的线程立即停止手上的任务,而是设置中断状态位,并且抛出InterruptedException异常。同样是上面那个例子,现在换成使用中断来取消任务:
public class InterruptExample extends Thread {
private BlockingQueue<BigInteger> bigIntegers = new LinkedBlockingQueue<>(10);
@Override
public void run() {
BigInteger temp = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted()) {
temp = temp.nextProbablePrime();
try {
bigIntegers.put(temp);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("end");
}
public void cancel() {
interrupt();
}
}
如果外部要取消这个任务,可以调用cancel方法提出中断请求。当被中断收到中断请求的时候,会将中断状态位置位,并抛出一个异常,这个异常可以在try-catch里被捕获并做处理。这种方法可以适应有阻塞操作的任务,故可以解决上述的问题。
捕获到InterruptedException异常之后怎么办?
一般来说有三种基本的处理方式:
- 生吞异常。即catch到了异常不做任何处理,就当没发生过一样,这往往不是一个好办法,甚至是一个非常糟糕的手段。
- 直接在catch块里做处理。这是一个不错的方法,但这要求必须有对应的中断处理策略(可以简单理解为中断服务例程),否则和生吞异常没有什么太大的区别。
- 恢复中断。即将中断标志位再次置位,使其处于中断状态。如上代码所示Thread.currentThread().interrupt();这样就能将中断传播,由上层的代码来解决中断问题。
三、利用Future来取消
Future是异步编程中常用到的,他的get方法会阻塞知道任务完成。get方法的一个重载可以指定超时时间,超时了就会抛出TimeoutException,这其实意味着我们已经不需要这个处理结果了,应当被取消。如下代码所示:
public void timedRun(Runnable runnable, long timeOut, TimeUnit timeUnit) {
Future<?> future = service.submit(runnable);
try {
future.get(timeOut, timeUnit);
} catch (TimeoutException e) {
//什么也不执行,但是最终会执行finally来取消
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
service.shutdown();
}
}
代码中try-catch-finally结构中,当超时方式时,会捕获到TimeoutException异常,但是这里不做任何处理,故代码最终回到finally里执行shutdown操作,平滑的取消任务。
四、非正常的线程终止
当线程发生异常的时候,有可能会导致整个程序直接停止,显然这是很危险的。受检异常是必须要try-catch的,可以有效的处理异常,但是对于非受检异常——例如RuntimeException——是有可能被遗漏的,所以可能会导致异常无法被正确处理,程序最终被迫停止。一个简单直接的方法是小心谨慎得对每个可能出现非受检异常的代码做try-catch处理,但这非常耗费精力,庞大的系统的代码非常多,任何地方都可能出现问题,这样一个地方一个地方的寻找非常麻烦,而且代码会非常杂乱。
另一种手段是使用类似Spring的“全局异常”。实现UncaughtExceptionHandler接口,这样线程出现异常的时候,如果没有任何处理,最终会被这个接口处理。可以在uncaughtException()方法里实现逻辑来处理异常,例如记录日志,关闭线程等。
五、JVM关闭
JVM既可以正常关闭(当所有非守护线程都结束的时候,或者调用System.exit),也可以强制关闭(linux下的 kill -9 或者 ctrl-c等)。JVM正常关闭的时候可以触发一个“关闭钩子”,这个钩子其实是一个回调,在JVM彻底关闭之前会调用钩子的逻辑,一般用来做一些释放资源的操作,例如关闭打开的文件等。如下代码所示:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("关闭钩子");
}));
如果JVM是非正常关闭(例如发生异常,程序被迫停止,或者用户调用kill来杀死进程等)是不会触发关闭钩子的。这点需要特别注意。
小结
正确,可靠,安全的取消任何和关闭线程是非常困难的,很难面面俱到,但我们可以汲取前辈的经验来构建出更加完善的关闭机制,从而提高系统的高可用性。