并发编程实战学习笔记(五)——取消与关闭

题记

在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 tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet());
    ....
    public List getCancelledTasks(){
        if(!exec.isTerminated()) throw new IllegalStateException(....);
        return new ArrayList(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就不会退出。

你可能感兴趣的:(Java,java,编程,并发,线程,安全)