到底如何设置线程池的核心线程数、最大线程数
线程池在业务中的实践
场景一:快速响应用户请求
这种场景可以将用户请求封装成任务并发执行,缩短总体响应时间,该场景需要获取最大的响应速度满足客户,应该不应该设置缓冲队列,缓冲并发任务。可以适当调高 corePoolSize 和 maxPoolSize 去尽可能创造多的线程快速执行任务。
场景2:快速处理批量任务
这种场景一般是需要大量执行离线任务,是吞吐量优先,但是不是要求瞬时完成,也就是要求尽可能在单位时间内处理更多的任务,可以使用缓冲队列,缓冲任务,corePoolSize 不适合特别大,太大频繁上下文切换,反而影响吞吐量。
业界的线程数配置方案一般都是比较理想化
并发任务的执行情况和任务类型相关,IO密集型和CPU密集型的任务运行起来的情况差异非常大,较难合理预估,这导致很难有一个简单有效的通用公式帮我们直接计算出结果。
CPU 密集型任务
比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。
核心线程数,可以设置为CPU核数 + 1 , +1是为了实现最优的利用率。即使当密集型的线程由于偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程也能确保 CPU 的时钟周期不会被浪费,从而保证 CPU 的利用率。
IO 密集型任务
比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。
核心数设置的一般比较多一些,因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,核心线程数=CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
尽管通过严谨的评估,依然很难一次计算出合适的参数,因此,可以换一个思路,把修改参数的成本降低,这样可以在告警发生时,快速调整,尽快恢复。
动态调整线程池参数
简化参数配置,关注核心参数,corePoolSize maxPoolSize workQueue,
- 延时优先的场景,同步队列。
 - 吞吐量优先的场景,使用有界队列。
 
修改 corePoolSize ThreadPoolExecutor#setCorePoolSize:在运行期间可以通过该方法修改 corePoolSize,会直接覆盖之前的 corePoolSize 值。
- 当前值小于之前值,表示有多余的 work 线程,此时会向当前 idle 的 worker 线程发起中断请求以实现回收,其余多余的 worker 在下次 idel 的时候也会被回收。
 - 当前值大于之前值,并且队列里有任务的时候,会创建新的线程执行任务。
 
修改 maxPoolSize ThreadPoolExecutor#setMaximumPoolSize
- 当前值小于之前值,超过的,并且已经在运行的线程会在 idle 的时候停止。
 
修改 workQueueSize
- LinkedBlockingQueue 没有开放修改 capacity 的方法,可以参考 LinkedBlockingQueue 自定义支持修改 capacity 的 Queue。
 
public class DynamicThreadPool {
    ThreadPoolExecutor threadPoolExecutor;
    public static void main(String[] args) throws InterruptedException {
        DynamicThreadPool threadPool = new DynamicThreadPool();
        threadPool.buildThreadPool();
        threadPool.print(threadPool.threadPoolExecutor, "init");
        for (int i = 0; i < 15; i++) {
            int finalI = i;
            threadPool.threadPoolExecutor.submit(() -> {
                threadPool.print(threadPool.threadPoolExecutor, "创建任务: " + finalI);
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        threadPool.modifyMaxSize(10);
        threadPool.modifyCoreSize(10);
        TimeUnit.SECONDS.sleep(2);
        threadPool.modifyWorkQueueSize(100);
    }
    public ThreadPoolExecutor buildThreadPool() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2,
                5,
                30,
                TimeUnit.MILLISECONDS,
                new ResizeLinkedBlockingQueue<>(10));
        this.threadPoolExecutor = executor;
        return executor;
    }
    public void modifyCoreSize(int num) {
        threadPoolExecutor.setCorePoolSize(num);
    }
    public void modifyMaxSize(int num) {
        threadPoolExecutor.setMaximumPoolSize(num);
    }
    public void modifyWorkQueueSize(int size) {
        ResizeLinkedBlockingQueue<Runnable> queue = (ResizeLinkedBlockingQueue<Runnable>) threadPoolExecutor.getQueue();
        queue.setCapacity(size);
    }
    public void print(ThreadPoolExecutor executor, String name) {
        ResizeLinkedBlockingQueue queue = (ResizeLinkedBlockingQueue) executor.getQueue();
        String message = String.format("%s 核心线程数: %s,最大线程数: %s,活动线程数: %s,完成任务数: %s,队列大小: %s,队列剩余: %s",
                name,
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getActiveCount(),
                executor.getCompletedTaskCount(),
                queue.size(),
                queue.remainingCapacity());
        System.out.println(message);
    }
}
问题一:线程池被创建后里面有线程吗?如果没有的话,你知道有什么方法对线程池进行预热吗?
默认情况线程池被创建后如果没有任务过来,里面是不会有线程的。如果需要预热的话可以调用下面的两个方法:
- prestartCoreThread 启动一个核心线程
 - prestartAllCoreThreads 启动所有的核心线程
 
问题二:核心线程数会被回收吗?需要什么设置?
核心线程数默认是不会被回收的,如果需要回收核心线程数,需要调用下面的方法:
- allowCoreThreadTimeOut(boolean value)
 
reference: