接上一篇《Java并发系列(12)——ForkJoin框架源码解析》
文章目录
9.5 线程池的选择与参数设置
9.5.1 JDK 预定义的线程池
9.5.1.1 Executors#newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
只传了 5 个参数,线程工厂和拒绝策略没有传。线程工厂不影响性能,拒绝策略比较重要。
拒绝策略不传就是默认,默认是 AbortPolicy,拒绝任务时抛异常:
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
特点:
- 队列使用了 SynchronousQueue,没有空间,不存储任务;
- 没有任务就没有线程;
- 任务并发量增大,线程不够立刻新建线程;
- 任务并发量降低,线程空闲 60 秒销毁;
- 拒绝策略不会触发,在拒绝策略触发前,程序会因为线程过多先挂掉。
适用场景(各条件为“且”的关系,下同):
- 任务耗时短;
- 非 cpu 密集型(会占用很多 cpu 资源)任务;
- 并发量时高时低;
- cpu 资源充足;
- 前提:能扛住高峰期最大并发量,程序不会挂掉。
此处,耗时多短算“短”,并发量多高算“高”,后面会提供一种计算思路。
9.5.1.2 Executors#newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点:
- 线程数量固定(除非线程池创建之后又改设置,并且不考虑懒加载的过程);
- 使用了 LinkedBlockingQueue 无界队列,队列容量无限;
- 拒绝策略不会触发,拒绝策略触发前,程序会先因为任务堆积,内存占用过多挂掉。
适用场景:
- 并发量比较稳定;
- 内存资源充足;
- 前提:偶尔并发激增,能扛住不会挂。
9.5.1.3 Executors#newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
特点:
- 队列使用 DelayedWorkQueue,无界队列;
- 可执行定时任务;
- 线程数量固定(除非线程池创建后又改设置);
- 拒绝策略触发前,任务堆积,内存溢出,程序先挂(但定时任务其实基本不存在任务堆积的问题)。
适用场景:
- 有定时任务需求。
9.5.1.4 Executors#newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
特点:
- 相当于 Executors#newFixedThreadPool(1);
- 区别是,线程数不会超过一个,因为线程池创建后不可再改设置 。
适用场景:
- 可用来控制任务有序执行;
- 禁止修改线程池参数。
9.5.1.5 Executors#newSingleThreadScheduledExecutor
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
特点:
- 相当于 Executors#newScheduledThreadPool(1);
- 区别是,线程数不会超过一个,因为线程池创建后不可再改设置。
适用场景:
- 有定时任务需求;
- 禁止修改线程池参数。
9.5.1.6 Executors#newWorkStealingPool
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
特点:
- 使用了 ForkJoinPool;
- 线程数量最多为 cpu 逻辑核心数(前提是正确使用,在讲 ForkJoin 源码的那一节中,我们分析过导致线程数量瞬间上万的情况);
- FIFO 模式。
适用场景:
- 有特殊需求不想跟其它业务共用 ForkJoin 的 common pool;
- 有特殊需求需要用 FIFO 模式(LIFO 模式性能更优)。
9.5.1.7 小结
本质上,JDK(8) 里面只有三个线程池:
- ThreadPoolExecutor;
- ScheduledThreadPoolExecutor;
- ForkJoinPool。
Executors 里面定义的 6 个线程池可以对号入座。
另外,Executors 里面定义的 6 个线程池各自都存在一些问题,一般不建议使用。当然,如果没什么并发量,任务也不复杂,随便怎么都行。
9.5.2 线程池的选择
线程池的选择问题,其实就是 ThreadPoolExecutor,ScheduledThreadPoolExecutor,ForkJoinPool 三选一的问题。
ThreadPoolExecutor 是应用面最广的,能应付大多数情况。下面探讨一下什么情况使用 ScheduledThreadPoolExecutor 或 ForkJoinPool 能带来压倒性优势。
9.5.2.1 ThreadPoolExecutor Vs ScheduledThreadPoolExecutor
这两个线程池的比较很简单,因为 ScheduledThreadPoolExecutor 也就是多了定时调度功能的 ThreadPoolExecutor,所以只有涉及定时调度功能时才会用到 ScheduledThreadPoolExecutor。
9.5.2.2 ThreadPoolExecutor Vs ForkJoinPool
从功能上,ForkJoinPool 比 ThreadPoolExecutor 多了拆分子任务的功能,如果用 ThreadPoolExecutor 需要自己处理任务的拆分与合并,稍微麻烦一些。
从性能上,这个需要测试对比一下。
实现相同的目的:从 1 累加到 1000 亿。
ThreadPoolExecutor 实现:
package per.lvjc.concurrent.pool.efficiency;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class ThreadPoolExecutorTest1 {
private static long n = 1000_0000_0000L;
private static final int poolSize = 16;
//拆分任务数量
private static final int taskSize = 16;
//任务分段
private static final float segment = 1f / taskSize;
private static final ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(poolSize);
//由于任务数量较多,使用 CompletionService 提升获取任务结果的效率
private static final ExecutorCompletionService<Long> completionService = new ExecutorCompletionService<>(executor);
public static void main(String[] args) throws ExecutionException, InterruptedException {
//循环测试 15 次
for (int i = 0; i < 15; i++) {
long startTime = System.currentTimeMillis();
long result = sumOperation();
long endTime = System.currentTimeMillis();
System.out.println("thread pool executor sum:" + result + ", cost:" + (endTime - startTime) + " ms");
}
}
private static long sumOperation() throws ExecutionException, InterruptedException {
long currentStart = 0, sum = 0, i = 0;
SumTask currentTask;
//手动分段提交任务
while (i++ < taskSize - 1) {
long currentEnd = (long) (segment * i * n);
currentTask = new SumTask(currentStart, currentEnd);
completionService.submit(currentTask);
currentStart = currentEnd + 1