08_01_线程池的使用
线程池的使用
一、默认线程池的弊端
并非所有任务都能适用于默认的执行策略,比如:
- 依赖性任务
- 线程封闭的任务
- 长时间运行的任务
- 使用线程本地变量的任务
这类任务往往需要指定特定的执行策略,否则可能会产生活跃性问题。
1.1 线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么就有可能产生死锁。
比如说,在只有1个线程的线程池中运行依赖任务时,就能产生死锁。
代码示例:
1 | public class ThreadDeadlock { |
除非线程池容量无限大,否则只要任务之间有依赖,那么就存在饥饿死锁的可能。
解决饥饿死锁的最好办法就是:
- 不同类型的任务使用不同的线程池
- 依赖的任务分离开来,使用不同的线程池
只要避免在同一个线程池中运行依赖的任务,就能避免线程饥饿死锁。
1.2 长时间运行
有时候,应用程序对任务的响应时间是有要求的,比如 GUI 程序。
如果一个任务在线程池中长时间阻塞,那么即使不出现死锁,线程池的响应性也会变得很差。
这类问题很难解决,但有一种办法可以缓解问题:
- 限制任务等待资源的时间,不要无限制地等待
- 如果任务等待超时,那么就将任务移除或重新放回队列等待执行
这个缓解方案,关键是评估任务的最长执行时间,否则如果限定时间太短,那么任务有可能永远无法完成。
二、线程池的优化
只有当任务都是同类型,并且相互独立时,线程池的性能才能达到最优。
2.1 线程的创建和销毁
与线程池中线程的创建和销毁有关的因素:
- 核心线程数量(Core Pool Size)
- 最大线程数量(Maximum Pool Size)
- 线程存活时间(Keep-Alive Time)
线程什么时候创建?
- 线程池在初始化时,线程数量为 0,按需创建线程
- “线程池大小 < 核心线程数量”,当有任务到来时,就创建一个新的线程
- “线程池大小 == 核心线程数量”,如果有任务到来,就将任务放入队列中等待执行
- “核心线程数量 <= 线程池大小 < 最大线程数量”,且任务队列满了,就创建一个新的线程
- “线程池大小 > 最大线程数量”,且任务队列满了,那么就拒绝执行任务
线程什么时候销毁?
- “核心线程数量 < 线程池大小”,且线程的空闲时间超过了存活时间,就销毁线程
- 默认情况下,核心线程一旦被创建,就永远不会被销毁
- 如果设置了
allowCoreThreadTimeOut
参数,当核心线程的空闲时间超过了存活时间,也会被销毁
通过调节线程池的核心线程数量、最大线程数量、存活时间,可以帮助线程池回收空闲线程占用的资源。
2.2 线程数量
线程数量的相关问题:
- 避免“过大”和“过小”
- “过大”,容易出现资源耗尽、资源竞争过于激烈等问题
- “过小”,没有充分利用处理器资源,降低了吞吐率
线程数量影响因素:
- 计算环境:处理器数量?
- 资源预算:多大的内存?可用的内存?
- 任务特性:计算密集型?IO密集型?
核心线程数量?
- 当计算密集型任务时,在 N 个处理器的情况下,通常线程池大小为 N+1 时性能最优
- 当 IO 密集型任务时,必须先估算出任务等待时间和计算时间的比值 R,然后再计算线程数量
- 当 IO 密集型任务时,通常线程数量为 “处理器数量N * CPU使用率U * (1 + R)” 时性能最优
最大线程数量?
- 计算每个任务对自由的需求量
- 用资源的可用总量除以每个任务的需求量,得到线程池的大小上限
这些都只是理论上的计算,现实中使用时,可以先用这种方案预设线程池数量。
然后再对实际环境进行监控和分析,不断调节线程池的大小。
2.3 任务队列
任务队列类型的选择:
- 有界队列
- 避免资源耗尽
- 任务相互独立
- 控制任务执行顺序,FIFO?LIFO?Priority?
- 有界队列的大小需要和线程池大小一起调节
- 可以拒绝任务
- 无界队列
- 有足够的可用资源
- 任务之间存在依赖性,避免死锁
- 不能拒绝任务
- 同步移交
- 避免任务排队
- 线程池大小是无界的
- 可以拒绝任务
根据实际情况选择不同的队列。
2.4 拒绝策略
- 中止(AbortPolicy):直接报错,停止当前线程
- 抛弃(DiscardPolicy):直接丢弃当前任务
- 抛弃最老的(DiscardOldestPolicy):丢弃最老的任务
- 调度者运行(CallerRunsPolicy):在调用者线程执行任务
2.5 线程工厂
自定义线程工厂?
- 每当线程池需要新的线程时,都会调用线程工厂的 newThread 方法创建新的线程
- 默认的线程工厂方法会创建一个新的、非守护的线程,并且没有任何特殊配置信息
- 实现
ThreadFactory
接口来自定义线程工厂方法,创建自定义线程
不可配置线程池?
- 通过
Executors.unmodifiableThreadPool
方法可以封装现有的线程池 - 封装后使其只暴露
ExecutorService
接口,因此不能对它进行配置
线程安全策略?
- 通过
Executors.priviledgedThreadFactory
方法会返回一个有安全策略的线程工厂 - 线程工厂创建出来的线程,拥有与创建线程池的线程相同的访问权限、上下文权限
2.6 生命钩子
线程池的钩子?
beforeExecute
:在任务执行前调用afterExecute
:在任务执行后调用terminated
:在线程池关闭后调用
钩子的作用?
- 可以用于添加日志、计时、监视、统计信息收集等功能
- 可以用于释放资源、清理数据、发送通知等
代码示例:
1 | public class TimingThreadPool extends ThreadPoolExecutor { |