- 为什么要使用线程池
- ThreadPoolExecutor 线程池状态
- ThreadPoolExecutor 线程池主要参数
- ThreadPoolExecutor 线程池拒绝策略
- Executors 与常用线程池
- 使用案例
PS: ThreadPoolExecutor 源码分析请参考 Github
为什么要使用线程池
-
由线程池创建、调度、监控和销毁所有线程,控制线程数量,避免出现线程泄露,方便管理。
-
重复利用已创建的线程执行任务,不需要持续不断地重复创建和销毁线程,降低资源消耗。
-
直接从线程池空闲的线程中获取工作线程,立即执行任务,不需要每次都创建新的线程,从而提高程序的响应速度。
ThreadPoolExecutor 线程池状态
线程池状态主要有 RUNNING, SHUTDOWN, STOP, TIDYING, TERMINATED 五种,之间的关系如下图所示:
RUNNING:正常运行状态,接受新的任务(如果没有达到拒绝策略的条件)
SHUTDOWN:不接收新的任务,但是会继续运行正在线程中执行的任务和在队列中等待的任务
STOP:不接收新任务,也不会运行队列任务,并且中断正在运行的任务
TIDYING:所有任务都已经终止,workerCount 为0,当池状态为TIDYING时将会运行terminated()方法
TERMINATED:完全终止
ThreadPoolExecutor 线程池主要参数
构造一个 ThreadPoolExecutor 线程池主要需要以下六个参数:
corePoolSize:核心线程数,表示通常情况下线程池中活跃的线程个数;
maximumPoolSize:线程池中可容纳的最大线程数;
keepAliveTime:此参数表示在线程数大于 corePoolSize 时,多出来的线程,空闲时间超过多少时会被销毁;
workQueue:存放任务的阻塞队列;
threadFactory:创建新线程的线程工厂;
handler:线程池饱和(任务过多)时的拒绝策略
ThreadPoolExecutor 线程池拒绝策略
此类中默认实现了以下四种拒绝策略(继承自 RejectedExecutionHandler 类,实现 rejectedExecution 方法):
CallerRunsPolicy:在调用者线程(而非线程池中的线程)中直接执行任务
AbortPolicy(默认):直接抛出 RejectedExecutionException 异常。程序中需要处理好此异常,不然会影响后续任务的执行。
DiscardPolicy:直接忽略任务,不执行任何操作。
DiscardOldestPolicy:丢弃任务队列中最老的任务,并将对此任务执行 execute。
Executors 与常用线程池
Executors 类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了 ExecutorService 接口,常用以下四种线程池(不包括 ForkJoinPool):
newCachedThreadPool: 线程数量没有限制(核心线程数设置为 0,其实有,最大限制为 Integer.MAX_VALUE),如果线程等待时间过长(默认为超过 60 秒),该空闲线程会自动终止。
newFixedThreadPool: 指定线程数量(核心线程数和最大线程数相同),在线程池没有任务可运行时,不会释放工作线程,还将占用一定的系统资源。
newSingleThreadPool: 只包含一个线程的线程池(核心线程数和最大线程数均为 1),保证任务顺序执行。
newScheduledThreadPool: 定长线程池,定时及周期性执行任务。可指定核心线程数,最大线程数为 Interger.MAX_VALUE。
注意:尽量使用 ThreadPoolExecutor 而不是 Executors 创建线程池。
Executors 创建的线程池对象的弊端如下:
-
FixedThreadPool 和 SingleThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量请求,从而导致 OOM。
-
CacheThreadPool 和 ScheduledThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
使用案例
比较使用单线程和使用 (CPU + 1)个线程计算 10 次 1 + 2 + 3 + … + n 的时间消耗(简单测试),程序如下:
public class FixedThreadPoolAnalysis {
public static void main(String[] args) {
int n = 1000000000;
long startTime = System.nanoTime();
test.singleThread(n);
long endTime = System.nanoTime();
System.out.println("单线程计算 10 次 1 + 2 + ... + " + n + " 的时间消耗: " + (endTime - startTime) + "ns");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
startTime = System.nanoTime();
test.multiThread(n);
endTime = System.nanoTime();
System.out.println("多线程计算 10 次 1 + 2 + ... + " + n + " 的时间消耗: " + (endTime - startTime) + "ns");
}
private static FixedThreadPoolAnalysis test = new FixedThreadPoolAnalysis();
private void singleThread(int n) {
long sum = 0L;
for (int k = 0; k < 10; k++) {
for (int i = 1; i <= n; i++)
sum += i;
}
System.out.println(Thread.currentThread().getName() + " -> " + sum);
}
private void multiThread(int n) {
int cpus = Runtime.getRuntime().availableProcessors();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(cpus + 1);
int interval = n / (cpus + 1);
for (int k = 0; k < 10; k++) {
for (int i = 0; i < cpus; i++) {
int left = i * interval + 1;
int right = (i + 1) * interval;
Runnable task = new Run(left, right);
fixedThreadPool.execute(task);
}
}
fixedThreadPool.shutdown();
}
class Run implements Runnable{
private int left;
private int right;
Run(int left, int right) {
this.left = left;
this.right = right;
}
@Override
public void run() {
long sum = 0L;
for (int i = left; i <= right; i++) {
sum += i;
}
System.out.println(Thread.currentThread().getName() + " -> " + sum);
}
}
}
测试所用 CPU 为 8 个,结果如下:
n | 100 | 10000 | 1000000 | 100000000 | 1000000000 |
---|---|---|---|---|---|
单线程时间消耗(ns) | 72500 | 3321700 | 23855700 | 1338836300 | 12968975200 |
多线程时间消耗(ns) | 11469800 | 16967900 | 17161400 | 11226400 | 19137600 |
结果显示,当 n 较小时,由于创建线程的时间消耗影响较大,使用多线程比使用单线程计算同一个任务的时间消耗更大。当 n 逐渐增大时,使用多线程的优势较明显。(此处不会出现任务数过多导致 OOM,所以为了简便,使用 Executors 创建 FixedThreadPool。)