对线程池调优实践的探索

164 阅读3分钟

简介

本文以一名初级Java开发的视角,探究如何在实际工作中用好线程池这一工具。本人没有在实际生产中创建过线程池,本文仅为学习笔记使用。

理论与实践的差异

线程池中最重要的一个参数就是最大线程数

上网搜索“线程池调优”,你会发现下面这种公式:

CPU核心数如果是n。IO密集型任务maxCount应该设置为2n,CPU密集型任务maxCount应该设置为n。

或者更高级一点的如《Java并发编程实战》中给出的公式,会将“等待CPU的线程比例”等因素考虑进去。


想一想其实能发现,这些公式并不是能帮助我们用好线程池的“银弹”。

一个物理机中不止一个进程。一个JavaWeb应用中,Tomcat有线程池,有数据库连接池,各种中间件中有线程池。我们写的业务代码中的线程池与这些非业务代码中的线程池,会争夺的的同一台物理机上的资源。甚至同一台物理机中,不同进程之间也会争夺系线程需要的资源。

那我们应该如何设置线程池参数呢?如何用好线程池呢?

参数设置

先说好设置的:

  1. 拒绝策略

    • 如果要保证所有任务都执行要使用CallerRunsPolicy。
    • 如果任务是修改一个数据,而我们只关心最终的结果,可以使用DiscardOldestPolicy。
    • 打印某个无关紧要的日志用DiscardPolicy。
    • 默认策略是AbortPolicy。
  2. 常用任务队列

    • 我们希望任务快一点执行,那队列就不要用容量,使用SynchronousQueue。这时也要注意,将最大线程数设置大一点,避免频繁触发拒绝策略。
    • 任务之间有优先级,使用PriorityBlockingQueue。
    • 使用ArrayBlockingQueue要设置合理的队列大小,防止出现OutOfMemoryError

线程数

注意:本小结是我凭空想,未经过实践验证。

线程池中不好设置参数的就是coreCount、maxCount,非核心线程存活时间这三个了。

  • coreCount:通过长期观察线上线程的活动情况,设置coreCount。比如我们发现生产中处于Runnable状态的线程数为60,我们就设置coreCount为60。
  • maxCount:如果通过压测我们发现,最大线程数到200的时候,CPU使用率就到90%多了,那我们就不能继续加大最大线程数了,因为再高就会降低系统的稳定性。(不一定是CPU使用率90%,可能是内存使用率等其他指标)。

当然我上面所说的是业务代码中我们自己创建的线程池的线程数之和。

因为我们会为不同任务的执行,创建参数不同的线程池。所以上面提到的coreCount和maxCount都是多个线程池参数加和的。

至于具体到每一个线程池的参数如何配置,可能就是要高并发任务的线程池多分点,不太重要的少分点。

使用线程池

我们通常会使用Future配合线程池一起使用

public static Boolean async1(List<Integer> nums) throws ExecutionException, InterruptedException, TimeoutException {
    List<Future<Boolean>> futures=new ArrayList<>();
    for (Integer num : nums) {
        Callable<Boolean> task = () -> {
            // do something
            return num > 0;
        };
        futures.add(threadPool.submit(task));
    }
    for (Future<Boolean> future : futures) {
        if (!future.get()){
            return false;
        }
    }
     return true;
}

如果有多个任务需要配合,则可考虑使用CompletableFuture,详见:CompletableFuture原理与实践-外卖商家端API的异步化 - 美团技术团队 (meituan.com)

如果是定时任务就使用ScheduledThreadPoolExecutor,这没啥毛病不多说了。

其他

参考资料: