Java并发编程实战 第7章 取消与关闭

Java没有提供任何机制来安全地终止线程。但它提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。这种协作方法是必要的,我们很少希望某个任务、线程或服务立即停止,因为这种立即停止会使共享数据结构处于不一致的状态。在编写任务和服务时可以使用一种协作的方式:当需要停止时,它们首先会清楚当前旨在执行的工作,然后再结束。任务本身比发出取消请求的代码更清楚如何执行清除工作。行为良好的软件能很完善的处理失败、关闭和取消等过程。

7.1 任务取消

在Java中没有一种安全的抢占式方法来停止线程。只有一些协作式的救治,使请求取消的任务和代码都遵循一种协商好的协议。
例如设置某个取消标志,任务定期查看这个标志。为了使这个过程能可靠工作,需要将其设置为volatile类型。

7.1.1 中断

不可靠的取消操作将把生产者置于阻塞的操作中,例如任务:

while (!canelled) {
	queue.put(...);
}

此时生产者速度大于消费者速度,生产者的put方法将被阻塞,消费者希望取消任务,那么会设置cancelled,这时消费者停止消费,生产者保持put阻塞,无法再去检测cancelled标志。

一些特殊的阻塞库的方法支持中断。线程中断是一种协作机制,线程可以通过这种机制来通知另一个线程,告诉他在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。

每个线程都有一个boolean类型的中断状态,interrupt能中断目标线程,isInterrupted能返回目标线程的中断状态,interrupted能清除当前线程的中断状态并返回它之前的值。

阻塞库方法,例如Thread.sleep、Object.wait等,都会检查线程合适中断,并在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM不能保证阻塞方法检测到中断的速度,但实际上还是非常快的。

对中断操作的正确理解:并不会真正的中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己(取消点)。

通常,中断是实现取消的最合理方式。
如果可中断的阻塞方法的调用频率并不高,不足以获得足够的响应性,那么显示的检测中断状态能起到一定的帮助作用。

7.1.2 中断策略

最合理的中断策略:以某种形式的线程级取消操作或服务级取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。

任务不应该对执行该任务的线程的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。

如果除了将InterruptedException传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();

每个线程拥有各自的中断策略,除非你知道中断对该线程的含义,否则就不应该中断这个线程。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如shutdown方法。

7.1.3 响应中断

两种实用策略可用于处理InterruptedException:

  • 传递异常 (方法throws出去)从而使方法也成为可中断的阻塞方法
  • 恢复中断状态 (再次调用interrupt来恢复中断)从而使调用栈中的上层代码能够对其进行处理

只有实现了线程中断策略的代码才可以屏蔽中断请求。

如果代码中不会调用可中断的阻塞方法,可以通过任务代码中轮询当前线程的中断状态来响应中断,选择合适的轮询频率是关键。

7.1.4 示例:计时运行

没看懂

7.1.5 通过Future来实现取消

如果任务在被取消前就抛出一个异常,那么该异常将被重新抛出以便由调用者来处理异常。

当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。

7.1.6 处理不可中断的阻塞

再看一遍

7.1.7 采用newTaskFor来封装非标准的取消

写一遍

7.2 停止基于线程的服务

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

7.2.1 示例:日志服务

没看懂

7.2.2 关闭ExecutorService

shutdown:正常关闭
shutdownNow:强行关闭

7.2.3 “毒丸”对象

当消费者获取到毒丸对象时,立即停止。生产者提交毒丸对象后,不会再提交别的工作。适用于FIFO队列。

可以扩展到多个生产者消费者。但是只有在无界队列中,毒丸对象才能可靠的工作。

7.2.4 示例:只执行一次的服务

7.2.5 shutdownNow的局限性

强行关闭时,会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,写入日志或保存起来。
我们无法通过常规方法找出哪些任务已经开始尚未结束,除非任务本身会执行某种检查。

幂等:Idempotent,任务执行两次与执行一次会得到相同的结果。

7.3 处理非正常的线程终止

导致线程提前死亡最主要的原因就是RuntimeException。由于这些异常表示出现了某种编程错误,或者其他不可修复的错误,因此通常不会被捕获。就不会再调用栈中逐层传递,而是默认地在控制太重输出栈追踪信息,并终止线程。

线程非正常退出的后果不确定,取决于线程在应用中的作用。

当编写一个向线程池提交任务的工作者线程类时,或者条用不可信的外部代码时,应该在try-catch中调用这些任务,捕获未检查的异常,或者可以使用try-finally代码块确保框架能够知道线程非正常退出的情况,并作出正确的响应。

未捕获异常的处理UncaughtExceptionHandler
Thread API中提供了UncaughtExceptionHandler,处理未捕获异常,能检测出异常终结的情况,能有效防止线程泄漏问题。
默认的UncaughtExceptionHandler是将栈追踪信息输出到System.err。
可以自定义,比如记日志,重启线程,关闭应用程序等。

execute提交的任务,可以通过UncaughtExceptionHandler来处理;
submit提交的任务,抛出的异常会被认为是任务返回状态的一部分,会被Future.get封装在ExecutionException中重新抛出。

7.4 JVM关闭

关闭:当最后一个正常(非守护)线程结束时;或调用了System.exit时;或通过其他平台特定的关闭方法时(Ctrl+C);或Runtime.halt,或在OS中杀死JVM进程(SIGKILL)。

7.4.1 关闭钩子

没看懂

7.4.2 守护线程

如果希望创建一个线程来执行一些辅助工作,但又不希望这线程阻碍JVM的关闭,这种情况需要使用守护线程(Daemon Thread)。

JVM启动是时创建的除了主线程,其他都是守护线程(例如GC)。当创建一个线程时,新线程继承创建它的线程的守护状态。因此默认,主线程创建的都是普通线程。

  • 普通线程(非守护线程)

  • 守护线程
    当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,JVM会正常退出操作。当JVM停止时,所有的守护线程将被抛弃——既不会执行finally代码块,也不会执行回卷栈,直接退出。
    尽量少使用守护线程,特别是包含IO,很难在不进行清理下安全的抛弃。最好用于执行内部任务,例如周期性从内存的缓存中移除逾期的数据。

7.4.3 终结器

finalize

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值