ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

线程池七大参数背得熟,这3个坑你却答不上来

线程池七大参数背得熟,这3个坑你却答不上来 线程池的七大参数——corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler——但凡准备过面试的人都能倒背如流。但背得熟和用得对之间隔着一道巨大的鸿沟。下面这三个坑每一个都曾在真实生产环境中让系统悄悄崩溃而面试时能答上来的人不到三成。坑一无界队列让maximumPoolSize形同虚设很多人以为只要设置了maximumPoolSize线程池在任务多的时候就会自动扩容到最大线程数。但线程池创建新线程的条件是当前线程数小于corePoolSize或者队列已满且当前线程数小于maximumPoolSize。注意后半句的关键词“队列已满”。如果你用的是LinkedBlockingQueue且没有指定容量它的默认容量是Integer.MAX_VALUE——一个永远不会满的队列。这意味着无论提交多少任务队列永远“没满”线程池永远不会创建超过corePoolSize的线程maximumPoolSize从头到尾都是个摆设。更危险的是任务会在这个无界队列里无限堆积直到堆内存耗尽抛出OOM。Executors.newFixedThreadPool()正是这个坑的典型代表这也是大厂Java规范明令禁止使用Executors创建线程池的核心原因。解法手动使用ThreadPoolExecutor传入有界队列如ArrayBlockingQueue并合理设置队列容量。队列满后会触发扩容到maximumPoolSize再满则执行拒绝策略让问题在可控范围内暴露。坑二扩容顺序是先核心、再队列、最后最大这是线程池最反直觉的设计。任务提交后的处理顺序是当前线程数 corePoolSize直接创建新线程执行任务核心线程已满任务放入workQueue排队队列已满创建新线程直到线程数达到maximumPoolSize队列满且线程数达到最大执行拒绝策略。很多人误以为“核心满了就创建新线程直到最大然后再排队”。实际顺序恰恰相反——队列的优先级高于最大线程数。这个顺序带来的直接后果是如果你的队列容量设得很大比如1000那么即使突发流量需要200个线程同时处理线程池也只会用corePoolSize个线程慢慢消费队列maximumPoolSize设置的100根本不会被触发。响应时间急剧上升但线程数纹丝不动。解法根据业务场景权衡队列容量和最大线程数。如果希望突发流量时快速扩容队列容量要设小如果希望缓冲平滑流量队列可以大一些但必须接受最大线程数可能长期用不到的現實。坑三submit提交的任务异常会被静默吞掉线程池提交任务有两个方法execute()和submit()。前者接收Runnable后者接收Runnable或Callable并返回Future。区别在于异常处理。execute()执行的任务如果抛出未捕获异常会触发线程的UncaughtExceptionHandler默认在控制台打印堆栈信息你能看到问题。但submit()会把异常封装在返回的Future中只有调用future.get()时才会以ExecutionException形式抛出。如果你submit了一个任务却从不调用future.get()——这在“提交后不关心结果”的场景中很常见——那么任务执行时抛出的任何异常都会悄无声息地消失。日志里什么都没有线程池也不会报错问题就像从未发生过一样。线上排查时你只会看到任务“没执行”却找不到任何线索。解法如果不需要返回值优先用execute()如果必须用submit()要么在任务内部用try-catch兜底要么在提交后调用future.get()获取异常要么自定义ThreadFactory设置UncaughtExceptionHandler。最忌讳的是submit之后既不get也不catch把异常当空气。写在最后线程池的参数只是表象真正决定它行为的是参数之间的交互逻辑。无界队列、扩容顺序、异常吞噬——这三个坑的共同点是它们都不违反任何一条参数的“字面意思”却能在生产环境中制造出最隐蔽的故障。背参数是入门理解参数之间的联动关系才算真正会用线程池。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进