任务取消
中断
线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
public class Thread{
public void interrupt() {
//......
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
}
每个线程都有一个boolean类型的中断状态,当中断线程时,这个线程的中断状态设置为true。
如上程序清单,interrupt方法能中断目标线程,而isInterrupted方法能返回目标线程的中断状态。静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。
阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现中断时提前返回。
它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。
调用interrupt并不意味着立即停止目标线程只在执行的工作,而只是传递了请求中断的消息。
中断操作并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。有些方法,例如wait、sleep、join等,将严格地处理这种请求,当它们收到中断请求或者开始执行时发现某个已被设置好的中断状态时,将抛出一个异常。
通常,中断是实现取消的最合理方式。
中断策略
最合理的中断策略是某种形式的线程级取消操作或者服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
任务不会在其自己拥有的线程中执行,而是在某个服务拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应。
这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应。它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或者库函代码实现了最合理的取消策略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。
由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
响应中断
当调用可中断的阻塞函数时,例如Thread.sleep或BlockingQueue.put等,有两种实用策略可用于处理InterruptedException:
- 传递异常
- 恢复中断状态
只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
对于一些不支持取消但可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方法,并在发现中断后重新尝试。在这种情况下,它们应该在本地保存中断状态,并在返回前恢复状态而不是捕获InterruptedException时恢复状态,如下代码所示:
public Task getNextTask(BlockingQueue<Task> queue){
boolean interrupted = false;
try{
while(true){
try{
return queue.take();
}catch(InterruptedException e){
interrupted = true;
//重新尝试
}
}
}finally{
if(interrupted){
Thread.currentThread().interrupt();
}
}
}
如果过早地设置中断状态,就可能引起无限循环,因为大多数可中断的阻塞方法都会在入口处检查中断状态,并且发现该状态已被设置就会立即抛出InterruptedException。
示例
通过Future来实现取消
处理不可中断的阻塞
在Java库中许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,但是并非所有的可阻塞的方法或者阻塞机制都能响应中断:
- Java.io包中的同步Socket I/O. 虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但是通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。
- Java.io包中的同步I/O. 当中断一个正在InterruptibleChannel上等待的线程时,将抛出CloseByInterruptException并关闭链路。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。
- Select的异步I/O. 如果一个线程在调用Selector.select方法时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
- 获取某个锁. Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。
停止基于线程的服务
处理非正常的线程终止
JVM关闭
关闭钩子
通过注册一个关闭钩子来停止日志服务:
public void start(){
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try{
LogService.this.stop();
}catch(InterruptedException ignored){
}
}
});
}
守护线程
有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍JVM的关闭。在这种情况下就需要使用守护线程(Daemon Thread)。
线程分为两种,普通线程和守护线程。
在JVM启动时创建的所有线程中,除了主线程外,其他线程都是守护线程。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。
普通线程与守护线程的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃 – 既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。