六、取消与关闭

任务和线程的启动很容易。在大多数时候,我们都会让它们运行直到结束,或者让它们自 行停止。然而,有时候我们希望提前结束任务或线程,或许是因为用户取消了操作,或者应用 程序需要被快速关闭。
要使任务和线程能安全、快速、可靠地停止下来,并不是一件容易的事。Java没有提供任 何机制来安全地终止线程。但它提供了中断(Interruption),这是一种协作机制,能够使一个 线程终止另一个线程的当前工作。
这种协作式的方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立 即停止会使共享的数据结构处于不一致的状态。相反,在编写任务和服务时可以使用一种协作 的方式:当需要停止时,它们首先会清除当前正在执行的工作,然后再结束。这提供了更好的 灵活性,因为任务本身的代码比发出取消请求的代码更清楚如何执行清除工作。
生命周期结束(End-of-Lifecycle)的问题会使任务、服务以及程序的设计和实现等过程变 得复杂,而这个在程序设计中非常重要的要素却经常被忽略。一个在行为良好的软件与勉强运 行的软件之间的最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。

一、任务取消

如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取(Cancellable)。取消某个操作的原因很多:

  • 用户请求取消。用户点击图形界面程序中的“取消”按钮,或者通过管理接口来发出取消请求,例如 JMX (Java Management Extensions)。
  • 有时间限制的操作。例如,某个应用程序需要在有限时间内搜索问题空间,并在这个时间内选择最佳的解决方案。当计时器超时时,需要取消所有正在捜索的任务。
  • 应用程序事件。例如,应用程序对某个问题空间进行分解并捜索,从而使不同的任务可以 捜索问题空间中的不同区域。当其中一个任务找到了解决方案时,所有其他仍在捜索的任务都将被取消。
  • 错误。网页爬虫程序捜索相关的页面,并将页面或摘要数据保存到硬盘。当一个爬虫任务发生错误时(例如,磁盘空间已满),那么所有捜索任务都会取消,此时可能会记录它们的当前状态,以便稍后重新启动。
  • 关闭。当一个程序或服务关闭时,必须对正在处理和等待处理的工作执行某种操作。在平 缓的关闭过程中,当前正在执行的任务将继续执行直到完成,而在立即关闭过程中,当前的任 务则可能取消。
    在Java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止 任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。

其中一种协作机制能设置某个“已请求取消(Cancellation Requested)”标志,而任务将定 期地查看该标志。如果设置了这个标志,那么任务将提前结束。程序清单中就使用了这项 技术,其中的PrimeGenerator持续地枚举素数,直到它被取消。cancel方法将设置cancelled标 志,并且主循环在捜索下一个素数之前会首先检查这个标志。(为了使这个过程能可靠地工作, 标志cancelled必须为volatile类型。)

public class PrimeGenerator implements Runnable {
    private final List<BigInteger> primes = new ArrayList<>();
    private volatile boolean cancelled;

    @Override
    public void run() {
        BigInteger p = BigInteger.ONE;
        while(!cancelled){
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel(){ // 最好放入finally块中执行
        cancelled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<>(primes);
    }

    public static void main(String[] args) {

        PrimeGenerator primeGenerator = new PrimeGenerator();
        new Thread(primeGenerator).start();

        try{
            Thread.sleep(1000*10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            primeGenerator.cancel();
        }

        System.out.println(primeGenerator.get());

    }
}
  • 中断

PrimeGenerator中的取消机制最终会使得捜索素数的任务退出,但在退出过程中需要花费 一定的时间。然而,如果使用这种方法的任务调用了一个阻塞方法,例如BlockingQueue.put, 那么可能会产生一个更严重的问题——任务可能永远不会检查取消标志,因此永远不会结束。
BrokenPrimeProducer就说明了这个问题。生产者线程生成素数,并 将它们放入一个阻塞队列。如果生产者的速度超过了消费者的处理速度,队列将被填满,put 方法也会阻塞。当生产者在put方法中阻塞时,如果消费者希望取消生产者任务,那么将发生 什么情况?它可以调用cancel方法来设置cancelled标志,但此时生产者却永远不能检查这个 标志,因为它无法从阻塞的put方法中恢复过来(因为消费者此时已经停止从队列中取出素数, 所以put方法将一直保持阻塞状态)。

public class BrokenPrimeProducer extends Thread{

    private final BlockingQueue<BigInteger> queue;
    private volatile boolean cancelled = false;

    public BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
        this.queue = queue;
    }

    @Override
    public void run(){
        try{
            BigInteger p = BigInteger.ONE;
            while(!cancelled){
                queue.put(p= p.nextProbablePrime());
            }
        } catch (InterruptedException e) {}
    }

    public void cancel() {
        cancelled = true;
    }

    public void consumePrimes() throws InterruptedException {
        BlockingQueue<BigInteger> primes = new ArrayBlockingQueue<>(10);
        BrokenPrimeProducer producer = new BrokenPrimeProducer(primes);
        producer.start();

        try{
            while(needMorePrimes()){
                consume(primes.take());
            }
        }finally {
            producer.cancel();
        }

    }

}

一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以 通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。
在Java的API或译言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,郡么都是不合适的,并且很难支撑起更大的应用。

每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设为true。在Thread中包含了中断线程以及査询线程中断状态的方法。interrupt方法能中断目标线程,而islnterrupted方法能返回目标线程的中断状态。静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。

public class Thread{
	public void interrupt(){...}
	public boolean isInterrupdate(){...}
	public static boolean interrupted(){...}
	...
}

阻塞库方法,例如Thread.sleep和Object.wait等,都会检査线程何时中断,并且在发现中 断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException, 表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。
当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检 查中断状态以判断发生了中断。通过这样的方法,中断操作将变得“有黏性”——如果不触发 InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。

对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)。有些方法,例女口 wait、sleep和join等,将严格地处理这种请求,当它们收到中断请求或者在开始执行时发现某 个已被设置好的中断状态时,将抛出一个异常。设计良好的方法可以完全忽略这种请求,只要 它们能使调用代码对中断请求进行某种处理。设计糟糕的方法可能会屏蔽中断请求,从而导致 调用栈中的其他代码无法对中断请求作出响应。

在使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态。如果在调用 interrupted时返回了 true,那么除非你想屏蔽这个中断,否则必须对它进行处理–可以抛出InterruptedException,或者通过再次调用interrupt来恢复中断状态。

BrokenPrimeProducer说明了一些自定义的取消机制无法与可阻塞的库函数实现良好交互的原因。如果任务代码能够响应中断,那么可以使用中断作为取消机制,并且利用许多库类中提 供的中断支持。
通常,中断是实现取消的最合理方式。

BrokenPrimeProducer中的问题很容易解决(和简化);使用中断而不是boolean标志来请 求取消,如一下程序清单所示。在每次迭代循环中,有两个位置可以检测出中断:在阻塞的put 方法调用中,以及在循环开始处査询中断状态时。由于调用了阻塞的put方法,因此这里并 不一定需要进行显式的检测,但执行检测却会使PrimeProducer对中断具有更髙的响应性, 因为它是在启动寻找素数任务之前检查中断的,而不是在任务完成之后。如果可中断的阻塞 方法的调用频率并不髙,不足以获得足够的响应性,那么显式地检测中断状态能起到一定的 帮助作用。

public class PrimeProducer extends Thread{

    private final BlockingQueue<BigInteger> queue;

    public PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run(){
        try{
            BigInteger p = BigInteger.ONE;
            while(!Thread.currentThread().isInterrupted()){
                queue.put(p = p.nextProbablePrime());
            }
        }
        catch (InterruptedException consumend){
            // 允许线程退出
        }
    }

    public void cancel(){
        interrupt();
    }

}
  • 中断策略

正如任务中应该包含取消策略一样,线程同样应该包含中断策略。中断策略规定线程如何 解释某个中断请求一当发现中断请求时,应该做哪些工作(如果需要的话),哪些工作单元 对于中断来说是原子操作,以及以多快的速度来响应中断。

最合理的中断策略是某种形式的线程级(Thread-Level)取消操作或服务级(Service-Level) 取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。此外还可以建立其他的中断策略,例如暂停服务或重新开始服务,但对于那些包含非标准中断策略的线程或线 程池,只能用于能知道这些策略的任务中。

区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者——中 断线程池中的某个工作者线程,同时意味着“取消当前任务”和“关闭工作者线程”。

任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码), 应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应,即使“非所有者”代码 也可以做出响应。(当你为一户人家打扫房屋时,即使主人不在,也不应该把在这段时间内收到的邮件扔掉,而应该把邮件收起来,等主人回来以后再交给他们处理,尽管你可以阅读他们的杂志。)

这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应。,它们永远不.会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策 略:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。

检査到中断请求时,任务并不需要放弃所有的操作——它可以推迟处理中断请求,并直 到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或 者表示已收到中断请求。这项技术能够确保在更新过程中发生中断时,数据结构不会被破坏。

任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务 中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消,还是其他某个中断 响应操作,都应该小心地保存执行线程的中断状态。如果除了将InterruptedException传递给调用 者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();

正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策 略信息封装到某个合适的取消机制中,例如关闭(shutdown)方法。

由于每个线程拥有各自的中断策略,因此睁非你知道中断对该线程的含义,否则就^ 不应该中断这个线程。

  • 响应中断

当调用可中断的阻塞函数时,例如Thread.sleep或BlockingQueue.put等,有两种实用策略可用于处理InterruptedException:

  • 传递异常(可能在执行某个特定于任务的清除操作之后),从而使你的方法也成为可中断的阻塞方法。
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。
    传递 InterruptedException 与将 InterruptedException 添加到 throws 子句中一样容易,如程序清中的getNextTask所示。
BlockingQueue<Task> queue;
...
public Task getNextTask() throws InterruptedException(){
    return queue.take();
}
...

如果不想或无法传递InterruptedException (或许通过Runnable来定义任务),那么需要寻找另一种方式来保存中断请求。一种标准的方法就是通过再次调用interrupt来恢复中断状态。 你不能屏蔽InterruptedException,例如在catch块中捕获到异常却不做任何处理,除非在你的代码中实现了线程的中断策略。虽然PrimeProducer屏蔽了中断,但这是因为它已经知道线程将要结束,因此在调用栈中已经没有上层代码需要知道中断信息。由于大多数代码并不知道它们将在哪个线程中运行,因此应该保存中断状态。

只有实现了线程中断策略的代码才可以屏蔽中断请求。在常规的任务和代码库中都不应该屏蔽中断请求。

对于一些不支持取消但仍可以调用可中断阻塞方法的操作,它们必须在循环中调用这些方
法,并在发现中断后重新尝试。在这种情况下,它们应该在本地保存中断状态,并在返回前杉^ 复状态而不是在捕获InterruptedException时恢复状态,如程序清单所示。如果过早地设置 中断状态,就可能引起无限循环,因为大多数可中断的阻塞方法都会在入口处检査中断状态, 并且当发现该状态已被设置时会立即抛出InterruptedException。(通常,可中断的方法会在阻 塞或进行重要的工作前首先检查中断,从而尽快地响应中断)。

public Task getNextTask(BlockingQueue<Taskgt> queue) {
boolean interrupted = false;
    try {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException e) { interrupted = true;
                //重新尝试
            }
        }
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中 断状态来响应中断。要选择合适的轮询频率,就需要在效率和响应性之间进行权衡。如果响应 性要求较高,那么不应该调用那些执行时间较长并且不响应中断的方法,从而对可调用的库代 码进行一些限制。
在取消过程中可能涉及除了中断犾态之外的其他状态。中断可以用来获得线程的注意,并 且由中断线程保存的信息,可以为中断的线程提供进一步的指示。(当访问这些信息时,要确 保使用同步。)
例如,当一个由ThreadPoolExecutoi•拥有的工作者线程检测到中断时,它会检査线程池是 否正在关闭。如果是,它会在结束之前执行一些线程池清理工作,否则它可能创建一个新线程 将线程池恢复到合理的规模。

  • 计时运行

许多问题永远也无烤解决(例如,枚举所有的素数),而某些问题,能很快得到答案,也可能永远得不到答案。在这些情况下,如果能够指定“最多花10分钟捜索答案”或者“枚举 出在10分钟内能找到的答案”,那么将是非常有用的。

  • 通过Future来实现取消

ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方 法带有一个boolean类型的参数maylntermptlfRunning,表示取消操作是否成功。(这只是表示 任务是否能够接收中断,而不是表示任务是否能检测并处理中断。)如果maylntermptlfRunning 为true并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。

除非你清楚线程的中断策略,否则不要中断线程,那么在什么情况下调用cancel可以将参 数指定为true ?执行任务的线程是由标准的Executor创建的,它实现了一种中断策略使得任务 可以通过中断被取消,所以如果任务在标准Executor中运行,并通过它们的Future来取消任 务,那么可以设置maylntemiptlfRunning。'当尝试取消某个任务时,不宜直接中断线程池,因 为你并不知道当中断请求到达时正在运行什么任务——只能通过任务的Future来实现取消。 这也是在编写任务时要将中断视为一个取消请求的另一个理由:可以通过任务的Future来取 消它们。

将任务提交给一个ExecutorService,并通 过一个定时的Future.get来获得结果。如果get在返回时抛出了一个Timeouffixception,那么任 务将通过它的Future来取消。如果任务在被取消前就抛出一个异常,那么该异常将被重新抛出以便由调用者来处理异常。一种良好的编程习惯:取消那些不再需要结果的任务。

static ExecutorService taskExec = Executors.newScheduledThreadPool(1);

public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException {

    Future<?> task = taskExec.submit(r);
    try{
        task.get(timeout, unit);
    }
    catch (TimeoutException e){
        // 接下来来任务将被取消
    }
    catch (ExecutionException e){
        // 如果在任务中抛出了异常,那么重新抛出该异常
        throw new ExecutionException(e.getCause());
    }
    finally {
        // 如果任务已经结束,那么执行取消操作也不会带来任何影响
            task.cancel(true);
    }
}
  • 处理不可中断的阻塞

并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O或者等待获得内置 锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。

Java.io包中的同步Socket I/O。在服务器应用程序中,最常见的阻塞.1/0形式就是对套接 字进行读取和写入。虽然InputStream和OutputStream中的read和write等方法都不会响应中 断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一 个 SocketException。

Java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛 出ClosedBylnterruptException并关闭链路(这还会使得其他在这条链路上阻塞的线程同样 抛出 ClosedBylnterruptException)。当关闭一个 InterruptibleChannel 时,将导致所有在链 路操作上阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了 InterruptibleChannel。

Selector 的异步 I/O。如果一个线程在调用 Selector.select 方法(在 java.nio.channels 中) 时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。

获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将无法响应中断,因为线程 认为它肯定会获得锁,所以将不会理会中断请求。但是,在Lock类中提供了 locklnterruptibly 方法,该方法允许在等待一个锁的同时仍能响应中断。

程序清单的ReaderThread给出了如何封装非标准的取消操作。ReaderThread管 理了一个套接字连接,它采用同步方式从该套接字中读取数据,并将接收到的数据传递给 processBuffer。为了结束某个用户的连接或者关闭服务器,ReaderThread改写了 interrupt方法, 使其既能处理标准的中断,也能关闭底层的套接字。因此,无论ReaderThread线程是在read 方法中阻塞还是在某个可中断的阻塞方法中阻塞,都可以被中断并停止执行当前的工作。

public class ReaderThread extends Thread {
    private final Socket socket;
    private final InputStream in;

    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = socket.getInputStream();
    }

    public void interrupt()	{
        try {
            socket.close();
        }
        catch (IOException ignored) { }
        finally {
            super.interrupt();
        }
    }


    public void run()	{
        try {

            byte [] buf = new byte [1024];
            while (true) {
                int count = in.read(buf);
                if (count < 0){
                    break;
                }
                else if (count > 0){
                    processBuffer(buf, count);
                }
            }
        } catch (IOException e) { /* 允许线程退出 */	}
    }
}
  • 采用newTaskFor来封裝非标准的取消

当把一个 Callable 提交给 ExecutorService 时, submit方法会返回一个Future,我们可以通过这个Future来取消任务。newTaskFor是一个工厂 方法,、它将创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,该接口扩展了Future 和 Runnable (并由 FutureTask 实现)。

通过定制表示任务的Future可以改变Future.cancel的行为。例如,定制的取消代码可 以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。通过改 写interrupt方法,ReaderThread可以取消基于套接字的线程。同样,通过改写任务的Future, cancel方法也可以实现类似的功能。

在程序清单的CancellableTask 中定义了一个CancellableTask 接口,该接口扩展了Callable, 并增加了一个 cancel 方法和一个 newTask 工厂方法来构造 RunnableFuture0 CancellingExecutor 扩展了 ThreadPoolExecutor,并通过改写newTaskFor使得CancellableTask可以创建自己的Future。

public interface CancellableTask<T> extends Callable<T> {
    void cancel();
    RunnableFuture<T> newTask();
}

public class CancellingExecutor extends ThreadPoolExecutor {
	
	....
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable){
        if (callable instanceof CancellableTask){
            return ((CancellableTask<T>)callable).newTask();
        }
        else {
            return super.newTaskFor(callable);
        }
    }
}

public abstract class SocketUsingTask<T> implements CancellableTask<T>{

    private Socket socket;


    protected synchronized void setSocket(Socket s){
        socket = s;
    }

    public synchronized void cancel(){
        try{
            if (socket != null){
                socket.close();
            }
        }
        catch (IOException e){}
    }

    public RunnableFuture<T> newTask(){
        return new FutureTask<T>(this){
            public boolean cancel(boolean mayInterruptIfRunning){
                try{
                    SocketUsingTask.this.cancel();
                }
                finally {
                    return super.cancel(mayInterruptIfRunning);
                }
            }
        };
    }
}

SocketUsingTask 实现了 CancellableTask,并定义了 Future.cancel 来关闭套接字和调用 super.cancelo如果SocketUsingTask通过其自己的Future来取消,那么底层的套接字将被关闭 并且线程将被中断。因此它提髙了任务对取消操作的响应性:不仅能够在调用可中断方法的同 时确保响应取消操作,而且还能调用可阻调的套接字I/O方法。

二、停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比 创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要 结束。由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。
正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如,中断线程 或者修改线程的优先级等。在线程API中,并没有对线程所有权给出正式的定义:线程由 Thread对象表示,并且像其他对象一样可以被自由共享。然而,线程有一个相应的所有者, 即创建该线程的类。因此线程池是其工作者线程的所有者,如果要中断这些线程,那么应该 使用线程池。
与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可 以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线 程。相反,服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线 程。这样,当应用程序关闭该服务时,服务就可以关闭所有的线程了。在ExecutorService中 提供了 shutdown和shutdownNow等方法。同样,在其他拥有线程的服务中也应该提供类似的关闭机制。

对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。

  • 关闭 ExecutorService

ExecutorService提供了两种关闭方法:使用shutdown正常关闭, 以及使用shutdownNow强行关闭。在进行强行关闭时,shutdownNow首先关闭当前正在执行 的任务,然后返回所有尚未启动的任务清单。

这两种关闭方式的差别在于各自的安全性和响应性:强行关闭的速度更快,但风险也 更大,因为任务很可能在执行到一半时被结束;而正常关闭虽然速度慢,但却更安全,因为 ExecutorService会一直等到队列中的所有任务都执行完成后才关闭。在其他拥有线程的服务中 也应该考虑提供类似的关闭方式以供选择。

  • “毒丸”对象

另一种关闭生产者-消费者服务的方式就是使用“毒丸(Poison Pill)”对象:“毒丸”是指 一个放在队列上的对象,其含义是:“当得到这个对象时,立即停止。”在FIFO (先进先出)队 列中,“毒丸”对象将确保消费者在关闭之前首先完成队列中的所有工作,在提交“毒丸”对 象之前提交的所有工作都会被处理,而生产者在提交了 “毒丸”对象后,将不会再提交任何工作。

  • shutdownNow 的局限性

当通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在执行的任务, 并返回所有已提交但尚未开始的任务,从而将这些任务写入日志或者保存起来以便之后进行处理。
然而,我们无法通过常规方法来找出哪些任务已经开始但尚未结束。这意味着我们无法在 关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查。要知道哪些任务还没 有完成,你不仅需要知道哪些任务还没有开始,而且还需要知道当Executor关闭时哪些任务正 在执行。

处理非正常的线程终止

当单线程的控制台程序由于发生了一个未捕获的异常而终止时,程序将停止运行,并产生 与程序正常输出非常不同的栈追踪信息,这种情况是很容易理解的。然而,如果并发程序中的 某个线程发生故障,那么通常并不会如此明显。在控制台中可能会输出栈追踪信息,但没有人 会观察控制台。此外,当线程发生故障时,应用程序可能看起来仍故在工作,所以这个失败很 可能会被忽略。幸运的是,我们有可以监测并防止在程序中“遗漏”线程的方法。
导致线程提前死亡的最主要原因就是RuntimeException。由于这些异常表示出现了某种编 程错误或者其他不可修复的错误,因此它们通常不会被捕获。它们不会在调用栈中逐层传递, 而是默认地在控制台中输出栈追踪信息,并终止线程。
线程非正常退出的后果可能是良性的,也可能是恶性的,这要取决于线程在应用程序中的 作用。虽然在线程池中丢失一个线程可能会对性能带来一定影响,但如果程序能在包含50个线程的线程池上运行良好,那么在包含49个线程的线程池上通常也能运行良好。然而,如果 在GUI程序中丢失了事件分派线程,那么造成的影响将非常显著——应用程序将停止处理事件 并且GUI会因此失去响应。
任何代码都可能抛出一个RuntimeException。每当调用另一个方法时,都要对它的行为保 持怀疑,不要盲目地认为它一定会正常返回,或者一定会抛出在方法原型中声明的某个已检査 异常。对调用的代码越不熟悉,就越应该对其代码行为保持怀疑。
在任务处理线程(例如线程池中的X作者线程或者Swing的事件派发线程等)的生命周期 中,将通过某种抽象机制(例如Runnable)来调用许多未知的代码,我们应该对在这些线程中 执行的代码能否表现出正确的行为保持怀疑。像Swing事件线程这样的服务可能只是因为某个 编写不当的事件处理器抛出NullPointerExc印tion而失败,这种情况是非常糟糕的。因此,这些 线程应该在try-catch代码块中调用这些任务,这样就能捕获那些未检查的异常了,或者也可以 使用try-fmally代码块来确保框架能够知道线程非正常退出的情况,并做出正确的响应。在这 种情况下,你或许会考虑捕获RuntimeException,即当通过Runnable这样的抽象机制来调用未 知的和不可信的代码时。

四、JVM关闭

JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式有多种,包括:当最后-- 个“正常(非守护)”线程结束时,或者当调用了 System.exit时,或者通过其他特定于平台的 方法关闭时(例如发送了 SIGINT信号或键入Ctrl-C)。虽然可以通过这些标准方法来正常关 闭JVM,但也可以通过调用Runtime.halt或者在操作系统中“杀死” JVM进程(例如发送 SIGKILL)来强行关闭JVM。

  • 关闭钩子

在正常关闭中,JVM首先调用所有已注册的关闭钩子(Shutdown Hook)。关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。 在关闭应用程序线程时,如果有(守护或非守护)线程仍然在运行,那么这些线程接下来将与 关闭进程并发执行。当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么 JVM将运行终结器,然后再停止。JVM并不会停止或中断任何在关闭时仍然运行的应用程序线 程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成,那 么正常关闭进程“挂起”并且JVM必须被强行关闭。当被强行关闭时,只是关闭JVM,而不 会运行关闭钩子。
关闭钩子应该是线程安全的:它们在访问共享数据时必须使用同步机制,并且小心地避免 发生死锁,这与其他并发代码的要求相同。而且,关闭钩子不应该对应用程序的状态(例如, 其他服务是否已经关闭,或者所有的正常线程是否已经执行完成)或者JVM的关闭原因做出 任何假设,因此在编写关闭钩子的代码时必须考虑周全。最后,关闭钩子必须尽快退出,因为 它们会延迟JVM的结束时间,而用户可能希望JVM能尽快终止。
关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由 操作系统自动清除的资源。

  • 守护线程

有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍JVM的关 闭。在这种情况下就需要使用守护线程(Daemon Thread)。
线程可分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。
普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时, JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。 当JVM停止时,所有仍然存在的守护线程都将被抛弃--既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。
我们应尽可能少地使用守护线程——很少有操作能够在不进行清理的情况下被安全地抛 弃。特别是,如果在守护线程中执行可能包含I/O操作的任务,那么将是一种危险的行为。守 护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。
此外,守护线程通常不能用来代替应用程序管理程序中给个服务的生命周期。

private static final ThreadFactory defaultDaemonThreadFactory = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = Executors.defaultThreadFactory().newThread(r);
        t.setDaemon(true);
        return t;
    }
};

ExecutorService service = Executors.newCachedThreadPool(defaultDaemonThreadFactory);
  • 终结器

当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式地交还给操作系统。为了实现这个功能,垃圾回收器对那些定义了 finalize方法的对象会进行特殊处理:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放。
由于终结器可以在某个由JVM管理的线程中运行,因此终结器访问的任何状态都可以被 多个线程访问,这样就必须对其访问操作进行同步。终结器并不能保证它们将在何时运行甚至 是否会运行,并且复杂的终结器通常还会在对象上产生巨大的性能开销。要编写正确的终结器 是非常困难的。在大多数情况下,通过使用finally代码块和显式的close方法,能够比使用 终结器更好地管理资源。唯一的例外情况在于:当需要管理对象,并且该对象持有的资源是通 过本地方法获得的。基于这些原因以及其他一些原因,我们要尽量避免编写或使用包含终结器 的类(除非是平台库中的类)。

应当避免使用终结器。


猜你喜欢

转载自blog.csdn.net/qq_27870421/article/details/90582987