【有书共读】《Java并发编程实战》第七章+第八章
第七章 取消与关闭
任务取消
- 如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以成为可取消的。
- Java中没有一种安全的抢占式方法来停止线程。
- 一个可取消的任务必须拥有取消策略,策略中详细定义取消操作的How,When以及What。
中断
- 通常,中断是实现取消的最合理方式。
- 中断的正确理解是,它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。
- 在取消之外的操作中使用中断,都是不合适的,并且很难支撑起更大的应用。
中断策略
- 最合理的中断策略是某种形式的线程级取消操作或服务及取消操作:尽快退出,在必要时进行清理,通知某个所有者该线程已经退出。
- 一个中断请求可以有一个或多个接受者。
- 大多数可阻塞库函数只抛出InterruptedException作为中断响应,也是最合理的取消策略:尽快退出执行流程并把中断信息传递给调用者,从而使栈中的上层代码可以采取进一步的操作。
- 由于每个线程拥有各自的中断策略,除非知道中断对该线程的含义,否则就不应该中断这个线程。
响应中断
- 两种实用策略处理InterruptedException,
- 传递异常
- 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理
- 只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。
- 如果代码不会调用可中断的阻塞方法,那么仍然可以通过在任务代码中轮询当前线程的中断状态来响应中断。
- join的不足:无法知道执行控制是因为线程正常退出而返回还是因为join超时而返回。
通过Future来取消
- 当尝试取消某个任务时,不宜直接中断线程池,因为并不知道当中断请求到达时正在运行什么任务,只能通过任务的Future来实现取消。
- 当Future.get抛出InterruptedException或TimeoutException时,如果知道不再需要结果,就可以调用Future.cancel来取消任务。
处理不可中断的阻塞
- 对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但要求知道线程阻塞的原因。
- 常见的不可阻塞中断包括
- Java.io包中的Socket I/O
- Java.io包中的同步I/O
- Selector的异步I/O
- 获取某个锁
采用newTaskFor来封装非标准化的取消
停止基于线程的服务
- 如果硬要程序准备退出,那么这些服务所拥有的线程也需要结束。
- 正确的封装原则是,除非拥有某个线程,否则不能对该线程进行操控。
- 对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么久应该提供生命周期方法。
关闭ExecutorService
- 强行关闭
- 正常关闭
“毒丸”对象
- “毒丸”指一个放在队列上的对象,当得到这个对象时,立即停止。
- 只要当生产者和消费者的数量都已知的情况下,才可以使用毒丸。
- 当生产者和消费者的数量较大时难以使用。
- 只有在***队列“毒丸”对象才能可靠的工作。
shutdownnow的局限性
- 无法通过常规方法来找出哪些任务已经开始但尚未结束。
处理非正常的线程终止
- 每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目地任务它一定会正常返回,或者一定会抛出在方法中声明的某个已检查异常。
- 在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。
JVM关闭
关闭钩子
- 指通过Runtime.addShutdownHook注册的但尚未开始的线程。
- 不应该依赖可能被应用程序或其他关闭钩子关闭的服务,对所有服务使用同一个关闭钩子,各个关闭操作串行执行。
守护线程
- 应尽可能少地使用守护线程,很少有操作能够在不进行清理的情况下被安全地抛弃。
- 守护线程通常不能用来替代应用程序管理中各个服务的生命周期。
终结器
- 避免使用。
第八章 线程池的使用
在任务与执行策略之间的隐性耦合
-
并非所有类型的任务都使用所有的执行策略,有些需要明确地指明:
- 依赖性任务
- 使用线程封闭的机制
- 对响应时间敏感的任务
- 使用ThreadLocal的任务
-
只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。
线程饥饿死锁
- 线程池中如果任务依赖于其他任务,那么可能产生死锁。
- 更大的线程池中,如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,称为线程饥饿死锁。
- 每当提交一个有依赖性的Executor任务时,要清楚的知道可能会出现线程饥饿死锁,因此需要在代码配置文件中记录线程池的大小限制或配置限制。
运行时间较长的任务
- 限定任务等待时间,而不要无限制等待。
设置线程池的大小
- 对于计算密集型任务,在拥有N_cpu个处理器的系统上,当线程池的大小为N_cpu+1时,通常能实现最优的利用率。
- 计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所得结果就是线程池大小的上限。
配置ThreadPoolExecutor
线程的创建与销毁
- 线程池的基本大小、最大大小以及存活时间等因素共同负责线程的创建与销毁。
管理队列任务
- 基本的任务排队方法有3种,***队列、有界队列和同步移交。
- 只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能线程饥饿死锁问题,此时应该使用***队列,比如newCachedThreadPool。
饱和策略
- 中止策略是默认的饱和策略。
- 抛弃最旧的策略会抛弃下一个将被执行的任务,然后尝试重新提交新任务。
- 调用者运行策略姜某些任务回退到调用者,从而降低新任务的流量。
- 工作队列被填满后,没有预定义的饱和策略来阻塞execute,可以通过信号量来限制任务的到达率。
线程工厂
- 默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。
- 许多情况下需要使用定制的线程工厂方法。