知道线程池先用核心线程、再排队、最后扩展线程以后,参数的含义就容易对应起来了。参数要一起看,单独把最大线程数调大不一定有效。

把队列容量写清楚

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class PoolSettingsDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger sequence = new AtomicInteger();
        ThreadFactory factory = task ->
                new Thread(task, "worker-" + sequence.incrementAndGet());
        ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 30,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(16), factory,
                new ThreadPoolExecutor.AbortPolicy());
        try {
            pool.execute(() -> System.out.println(Thread.currentThread().getName()));
        } finally {
            pool.shutdown();
        }
        if (!pool.awaitTermination(5, TimeUnit.SECONDS)) pool.shutdownNow();
    }
}

这里的 2、4、16 只是演示值。线程名称有助于从日志、堆栈里辨认任务来源,真正的容量则要结合任务耗时和机器资源确定。

keepAliveTime 控制可回收线程的空闲等待时间。默认核心线程不因这个时间而退出,调用 allowCoreThreadTimeOut(true) 后也可以回收,但需要正的 keepAliveTime。

队列影响扩容

newFixedThreadPool() 使用无界 LinkedBlockingQueue,线程数固定,积压主要进入队列。慢任务持续进入时,要关注内存和排队延迟。

newCachedThreadPool() 使用 SynchronousQueue,把任务直接交给线程,不能立即交付时可以创建新线程。它的最大线程数很大,突发任务可能造成大量线程。

有界队列让容量更明确,但也必须处理任务被拒绝的情况。队列只是缓冲,不能解决长期“进入速度大于处理速度”。

拒绝策略

策略行为
AbortPolicy抛出 RejectedExecutionException
CallerRunsPolicy未关闭时由提交线程执行,否则丢弃
DiscardPolicy静默丢弃
DiscardOldestPolicy未关闭时移除队头任务,再尝试提交

CallerRunsPolicy 可以拖慢生产者,但也会把工作放进请求线程、事件线程等提交方上下文。持锁提交任务时尤其要检查重入和等待关系。

静默丢弃与 submit 一起使用还有一个容易忽略的问题:返回的 Future 可能既不完成也不取消,调用方的 get 一直等不到结果。自定义拒绝策略应明确抛异常、记录失败或取消 Future,不能只写一条日志就算处理完。

DiscardOldestPolicy 移除的是队头,不一定是业务上最不重要的任务,配合优先级队列时尤其容易误判。

怎么估算

CPU 密集任务可以从接近可用核心数开始测;等待较多的任务可以适当增加线程,但还要受数据库连接、下游容量、内存等限制。可以先选一组参数,再通过实际负载观察吞吐量和等待时间。

观察活动线程数、队列长度、拒绝次数、等待时间和处理时间,才能知道瓶颈到底在执行还是排队。多个任务互相等待结果时,也要检查是否共用同一个已经耗尽的线程池。

最后修改:2018 年 04 月 15 日
如果觉得我的文章对你有用,请随意赞赏