线程池的七个参数,每个都在防什么
new ThreadPoolExecutor(...)那一串参数,抄示例的人多,想清楚的人少。一个真实的事故形态:核心线程数照抄 CPU 核数,任务队列用了默认的无界队列,高峰期任务堆到几十万个,内存先爆,拒绝策略一个都没触发,因为队列永远「没满」。七个参数每一个都在防一种灾难,填错一个,防线就悄悄消失了。
七个参数逐个拆
corePoolSize,核心线程数。常驻的工人数量,哪怕闲着也不回收(默认情况下)。它防的是「任务来了没人接」,定小了排队,定大了空转。
maximumPoolSize,最大线程数。忙不过来时临时扩编的上限。它防的是「任务暴增把机器线程打爆」,没有它,每个任务都能变成一个新线程,机器先于业务倒下。
keepAliveTime 加 unit,非核心线程的空闲存活时间。临时工干完活多久后离场。它防的是「高峰过后线程堆积着不走」,浪费内存和调度成本。
workQueue,任务队列。核心线程满后任务先排队的地方。它防的是「任务来了立刻扩编」,用排队换平稳。注意它得有界,无界队列等于把防线换成内存炸弹。
threadFactory,线程工厂。线程怎么造,名字、优先级、异常处理器都从这走。它防的是「排查时对着 Thread-17 一脸茫然」,线程命名是白给的可观测性。
rejectedExecutionHandler,拒绝策略。队列也满、线程也到顶之后的兜底动作。它防的是「任务被静默吞掉」,最后一个关口。
任务的完整路由
路由顺序是核心、队列、最大、拒绝。这里有个反直觉的点,队列排在扩容前面,所以无界队列会让 maximumPoolSize 永远轮不到出场,参数形同虚设。
四种拒绝策略
AbortPolicy,直接抛 RejectedExecutionException。默认策略,让调用方知道出事了。适合对丢任务零容忍的场景。
CallerRunsPolicy,谁提交谁执行。提交任务的线程自己跑,天然减速,把压力还给生产者。适合不许丢、也不怕慢的场景。
DiscardPolicy,静默丢掉最新的。最容易出事故的策略,任务消失得无影无踪。只适合真无所谓丢的场景,比如监控打点。
DiscardOldestPolicy,丢掉队列里最老的。腾位置给新任务,适合「新的比旧的值钱」的场景,比如实时行情。
这也是规约禁止 Executors 工厂方法的原因。newFixedThreadPool 的队列无界,OOM 典型;newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE,线程数能冲上天。自己手写 ThreadPoolExecutor,每个参数都过目,防线才真实存在。原则就一条:队列必须有界,拒绝必须有人看。
参数怎么定
两套经验口径,标成经验,别当公式。CPU 密集型,核心数加一,多的那个线程在页缺失时垫着。I/O 密集型,核心数乘二起步,或者按「核数除以一减等待占比」估。真正的定法是压测加观测,线程池的监控指标(活跃线程、队列长度、完成任务数)比任何公式都诚实。
亲手看一次路由
下面用一段 Java 起一个小池子加小队列,一口气提交 12 个任务,亲眼看到排队、扩编和拒绝的完整路由。直接运行java ThreadPoolDemo.java(JDK 11+)。
// 直接运行:java ThreadPoolDemo.java// 小池子加小队列,亲眼看任务路由和拒绝触发。importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.RejectedExecutionException;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.atomic.AtomicInteger;publicclassThreadPoolDemo{publicstaticvoidmain(String[]args){AtomicIntegerseq=newAtomicInteger();ThreadPoolExecutorpool=newThreadPoolExecutor(2,4,// 核心 2,最大 41,TimeUnit.SECONDS,// 非核心空闲 1 秒回收newArrayBlockingQueue<>(4),// 队列只装 4 个r->newThread(r,"biz-"+seq.incrementAndGet()),newThreadPoolExecutor.AbortPolicy()// 满了就拒绝);for(inti=1;i<=12;i++){finalinttaskId=i;try{pool.execute(()->{System.out.println(Thread.currentThread().getName()+" 开始任务 "+taskId);try{TimeUnit.MILLISECONDS.sleep(300);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}System.out.println(Thread.currentThread().getName()+" 完成任务 "+taskId);});System.out.println("任务 "+taskId+" 提交成功");}catch(RejectedExecutionExceptione){System.out.println("任务 "+taskId+" 被拒绝:"+e.getClass().getSimpleName());}}pool.shutdown();}}跑完看输出的分布,2 个核心线程先接活,接着 4 个进队列,再起 2 个非核心线程顶上,第 9 个任务起开始被拒绝。12 个任务,8 个被接,4 个被拒,数字和路由图完全对得上。记住一句话:线程池的路由决定任务的生死,参数决定路由走哪条路。
适用边界
这一篇管线程池本身。任务本身的超时取消(Future.cancel 与中断协作)、线程池隔离(舱壁模式)、以及虚拟线程对这七个参数的冲击,是后续的题目。参数的数值随业务形状变化,文中给的是估算口径,上线前压测定数。跑通演示后拿你线上的线程池配置对照一遍,哪个参数在裸奔,当场就看得见。