Java并发系列(13)——线程池的选择与参数设置

接上一篇《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
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值