知道线程池先用核心线程、再排队、最后扩展线程以后,参数的含义就容易对应起来了。参数要一起看,单独把最大线程数调大不一定有效。
把队列容量写清楚
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 密集任务可以从接近可用核心数开始测;等待较多的任务可以适当增加线程,但还要受数据库连接、下游容量、内存等限制。可以先选一组参数,再通过实际负载观察吞吐量和等待时间。
观察活动线程数、队列长度、拒绝次数、等待时间和处理时间,才能知道瓶颈到底在执行还是排队。多个任务互相等待结果时,也要检查是否共用同一个已经耗尽的线程池。