08_01_线程池的使用

线程池的使用

一、默认线程池的弊端

并非所有任务都能适用于默认的执行策略,比如:

  • 依赖性任务
  • 线程封闭的任务
  • 长时间运行的任务
  • 使用线程本地变量的任务

这类任务往往需要指定特定的执行策略,否则可能会产生活跃性问题。

1.1 线程饥饿死锁

在线程池中,如果任务依赖于其他任务,那么就有可能产生死锁。

比如说,在只有1个线程的线程池中运行依赖任务时,就能产生死锁。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadDeadlock {

// 单线程的线程池
private final ExecutorService exec = Executors.newSingleThreadExecutor();

public class RenderPageTask implements Callable<String> {
@Override
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// 将发生死锁 —— 由于任务在等待子任务的结果
return header.get() + page + footer.get();
}
}

}

除非线程池容量无限大,否则只要任务之间有依赖,那么就存在饥饿死锁的可能。

解决饥饿死锁的最好办法就是:

  • 不同类型的任务使用不同的线程池
  • 依赖的任务分离开来,使用不同的线程池

只要避免在同一个线程池中运行依赖的任务,就能避免线程饥饿死锁。

1.2 长时间运行

有时候,应用程序对任务的响应时间是有要求的,比如 GUI 程序。

如果一个任务在线程池中长时间阻塞,那么即使不出现死锁,线程池的响应性也会变得很差。

这类问题很难解决,但有一种办法可以缓解问题:

  • 限制任务等待资源的时间,不要无限制地等待
  • 如果任务等待超时,那么就将任务移除或重新放回队列等待执行

这个缓解方案,关键是评估任务的最长执行时间,否则如果限定时间太短,那么任务有可能永远无法完成。

二、线程池的优化

只有当任务都是同类型,并且相互独立时,线程池的性能才能达到最优。

2.1 线程的创建和销毁

与线程池中线程的创建和销毁有关的因素:

  • 核心线程数量(Core Pool Size)
  • 最大线程数量(Maximum Pool Size)
  • 线程存活时间(Keep-Alive Time)

线程什么时候创建?

  1. 线程池在初始化时,线程数量为 0,按需创建线程
  2. “线程池大小 < 核心线程数量”,当有任务到来时,就创建一个新的线程
  3. “线程池大小 == 核心线程数量”,如果有任务到来,就将任务放入队列中等待执行
  4. “核心线程数量 <= 线程池大小 < 最大线程数量”,且任务队列满了,就创建一个新的线程
  5. “线程池大小 > 最大线程数量”,且任务队列满了,那么就拒绝执行任务

线程什么时候销毁?

  • “核心线程数量 < 线程池大小”,且线程的空闲时间超过了存活时间,就销毁线程
  • 默认情况下,核心线程一旦被创建,就永远不会被销毁
  • 如果设置了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class TimingThreadPool extends ThreadPoolExecutor {

private final ThreadLocal<Long> startTime = new ThreadLocal<>();
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();

@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
System.out.printf("Thread %s: start %s%n", t, r);
startTime.set(System.nanoTime());
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
System.out.printf("Thread %s: end %s, time=%dns%n", t, r, taskTime);
} finally {
super.afterExecute(r, t);
}
}

@Override
protected void terminated() {
try {
System.out.printf("Terminated: avg time=%dns", totalTime.get() / numTasks.get());
} finally {
super.terminated();
}
}
}
作者

jiaduo

发布于

2022-05-15

更新于

2023-04-03

许可协议