题记
在Java中没有一种安全的抢占方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
响应中断时执行的操作包括
- 清除中断状态
- 抛出InterrruptedException,表示阻塞操作由于中断而提前结束
对中断操作的正确理解
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一合适的时刻中断自己。
论断
- 通常,中断是实现取消的最合理方式。
- 只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
- 使用线程池往往比直接new Thread会更加方便,因为线程池封装了相关特性,如线程封装,批量中断机制等。
代码响应方式
- 阻塞库方法,捕捉InterruptedException,做好清除工作,然后结束线程
- 自己的代码在合适的地方判断isInterrupted(),以响应中断
- 一些阻塞方法但不抛出InterruptedException的,需要具体讨论,参见下述
处理不可中断的阻塞
- Java.io包中的同步socket I/O。通过关闭底层的套接字,可以使得由于执行read或者write等方法而被阻塞的线程抛出一个SocketException。
- Java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出CloseByInterruptException并关闭链路
- Selector的异步I/O。如果一个线程在调用Selector.select方法(java.io.channels中)时阻塞了,那么调用close或者wakeup方法会使线程抛出ClosedSelectorException并提前返回。
- 获取一个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断。但在,在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁时仍能响应中断。
区分任务与线程对中断的反应是很重要的
你是中断任务还是中断线程,含义并不相同。比如在线程池里,你只是中断任务,处理完成异常,清除状态之后,只需要简单返回就行,但如果确定是要中断线程,则要进行其它考虑了。
响应中断
当调用可中断的阻塞函数时,有两种实用策略可用于处理InterruptedException
- 传递异常(可能在执行某个特定于任务的清除操作之后),从而使得你的方法也成为可中断的阻塞方法。
- 恢复中断状态,从而使得调用栈中的上层代码能够对其处理。
注意:只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和库代码中都不应该屏蔽中断请求。
线程池中的任务通过Future来实现取消,其中CANCEL API文档说明
- 返回结果:返回FALSE:任务已经取消,已经完成等不能取消的原因。否则返回true
- 如果任务未开始执行,则不再执行
- 如果任务已经在运行了,则取决于参数mayInterruptIfRunning,true则会被中断,false则不会中断
线程池shutdownnow的局限性:我们无法通过常规方法找出哪些任务已经开始但尚未结束
如果我们需要知道这些任务,并且不想直接中断而是进行继续的后续处理,则可以使用TrackingExecutor找出哪些任务已经开始但还没有正常完成。在Executor结束后,getCancelledTasks返回被取消的任务清单。
public class TrackingExecutor extends AbstractExecutorService{
private final ExecutorService exec;
private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
....
public List<Runnable> getCancelledTasks(){
if(!exec.isTerminated()) throw new IllegalStateException(....);
return new ArrayList<Runnable>(tasksCancelledAtShutdown);
}
public void executor(final Runnable runnable){
//重点看这一段代码
try{
runnable.run();
}finally{
if(isShutdown() && Thread.currentThread().isInterrupted())
tasksCancelledAtShutdown.add(runnable);
}
}
// 将ExecutorService其它方法委托给exec
}
//另一段使用TrackingExecutor类的代码
public synchronized void stop() throws InterruptedException{
try{
saveUncrawled(exec.shutdownNow());
if(exec.awaitTermination(TIMEOUT,UNIT))
saveUncrawled(exec.getCancelledTasks);
}finally{
exec = null;
}
}
非正常的线程终止处理办法
- 线程应该在try-catch块中调用这些任务,这样就能捕获那些未检查的异常了。或者也可以使用try-finally代码块来确保框架能够知道线程非正常退出的情况,并做出正确的响应。你或者会捕获RuntimeException异常,即当通过Runnable这样的抽象机制来调用未知和不可信的代码时。
- 在Thread API中同样提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况。
- 要为线程池中的所有线程设置一个UncaughtExceptionHandler,需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory.
- 令人困惑的是,只有通过execute提交的任务,才能将它抛出的异常交给未捕捉异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。
QUESTION:主线程退出,子线程没有退出,JVM会退出吗?
ANSWER:不会退出,因为只要有非daemon线程未退出,JVM就不会退出。