1. 线程池为何成为面试必考题
在Java后端开发的面试中,线程池几乎是绕不开的技术话题。这背后有几个深层原因:首先,现代服务基本都是高并发设计,线程作为系统调度的基本单位,其管理效率直接影响系统性能;其次,不当的线程使用会导致内存溢出、上下文切换过度等问题;再者,线程池的实现本身涉及队列、锁、线程调度等Java并发核心机制。
我见过不少候选人能说出线程池的四个创建方法,但当被问到"为什么阿里开发规范要求用ThreadPoolExecutor而不是Executors创建线程池"时却答不上来。这种知其然不知其所以然的情况,恰恰是面试官最想深挖的地方。
2. 线程池核心参数全解析
2.1 构造方法里的七个关键参数
ThreadPoolExecutor的完整构造方法如下:
java复制public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:核心线程数,相当于公司的正式员工。即使没有任务也不会被回收,除非设置allowCoreThreadTimeOut。我建议根据CPU核心数设置,比如N核服务器可以设为N或N+1。
-
maximumPoolSize:最大线程数,相当于正式工+临时工的总和。当队列满且当前线程数小于max时,会创建新线程。注意设置过大可能导致OOM。
-
keepAliveTime:非核心线程的空闲存活时间。就像临时工如果长时间没活干就会被辞退。
-
workQueue:任务队列,常用的有三种:
- LinkedBlockingQueue:无界队列,可能导致OOM
- ArrayBlockingQueue:有界队列,需要合理设置大小
- SynchronousQueue:不存储任务,直接移交
2.2 四种拒绝策略对比
当线程数达到max且队列已满时,会触发拒绝策略:
| 策略类 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛出RejectedExecutionException | 需要明确感知任务被拒绝 |
| CallerRunsPolicy | 由提交任务的线程自己执行 | 不希望丢失任务且可接受同步执行 |
| DiscardPolicy | 静默丢弃新任务 | 允许丢弃新任务的场景 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 | 允许丢弃老任务,保留新任务 |
实际开发中建议自定义拒绝策略,比如记录日志或存入数据库后续重试
3. 线程池工作原理深度图解
3.1 任务提交全流程
- 提交任务后首先判断当前线程数是否小于corePoolSize
- 如果小于,立即创建新线程执行任务(即使有其他空闲线程)
- 如果达到coreSize,将任务放入workQueue
- 如果队列已满且线程数小于maxPoolSize,创建新线程
- 如果线程数也达到max,触发拒绝策略
这个流程有个反直觉的点:不是先填满队列再创建新线程。实际上是在队列未满时就可能创建新线程(当线程数小于coreSize时)。
3.2 线程回收机制
- 非核心线程:超过keepAliveTime没有任务就会被回收
- 核心线程:默认不会回收,除非设置allowCoreThreadTimeOut=true
- 回收时是通过从队列poll任务时设置超时实现的
4. 四种工厂方法的陷阱
Executors提供的快捷方法其实都有隐藏问题:
-
newFixedThreadPool
使用LinkedBlockingQueue,队列无界可能导致OOM -
newSingleThreadExecutor
同样是无限队列,且线程数固定为1 -
newCachedThreadPool
最大线程数为Integer.MAX_VALUE,可能创建过多线程 -
newScheduledThreadPool
用于定时任务,但最大线程数也是无界的
阿里规约明确禁止使用这些方法,推荐通过ThreadPoolExecutor构造方法手动创建
5. 线上问题排查实录
5.1 线程池导致的OOM案例
某次大促前压测时,系统频繁Full GC。通过MAT分析发现:
- 线程池使用Executors.newFixedThreadPool(200)
- 任务处理较慢导致队列堆积了50万+任务
- 每个任务包含约1KB数据,光队列就占用了500MB+内存
解决方案:
- 改用ArrayBlockingQueue并设置合理大小
- 添加监控告警,当队列堆积超过阈值时报警
- 实现自定义拒绝策略将任务持久化到Redis
5.2 线程泄漏排查
某服务重启后线程数持续增长不释放。通过jstack发现:
- 线程池配置了allowCoreThreadTimeOut=true
- 但某些任务执行时间过长(超过1小时)
- 导致线程一直无法回收
修复方案:
- 设置合理的任务超时时间
- 使用Future.get(timeout)控制任务执行时间
- 对长时间任务改用单独的特殊线程池处理
6. 最佳实践与调优建议
6.1 参数设置公式
对于CPU密集型任务:
code复制coreSize = CPU核数 + 1
maxSize = coreSize * 2
queueSize = 100-1000
对于IO密集型任务:
code复制coreSize = CPU核数 * 2
maxSize = coreSize * (1 + avg_wait_time/avg_compute_time)
queueSize = 根据业务容忍度设置
6.2 监控关键指标
建议监控以下指标并设置告警:
- 活跃线程数/最大线程数
- 队列剩余容量
- 最近1分钟拒绝任务数
- 任务平均执行时间
可以通过ThreadPoolExecutor的以下方法获取:
java复制pool.getActiveCount()
pool.getQueue().size()
pool.getCompletedTaskCount()
6.3 Spring中的优雅使用
在Spring Boot中推荐这样配置:
java复制@Bean
public ThreadPoolExecutor bizPool() {
return new ThreadPoolExecutor(
10, 50,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("biz-pool"),
new LogRejectedPolicy());
}
配合@Async使用:
java复制@Async("bizPool")
public void asyncProcess(Order order) {
// 业务处理
}
7. 高频面试题深度解析
7.1 为什么线程池能提高性能?
- 降低资源消耗:线程创建/销毁开销大,复用已有线程
- 提高响应速度:任务到达时可以直接执行,无需等待线程创建
- 提高线程可管理性:统一分配、调优和监控
7.2 核心线程会被回收吗?
默认不会,但有两种特殊情况:
- 设置allowCoreThreadTimeOut=true时会被回收
- 线程池关闭时(shutdown/shutdownNow)
7.3 如何正确关闭线程池?
错误做法:
- 直接kill进程:可能导致任务丢失
- 不调用shutdown:可能造成线程泄漏
推荐流程:
java复制executor.shutdown(); // 停止接收新任务
if(!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 取消正在执行的任务
if(!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未正常关闭");
}
}
7.4 线程池适合处理什么类型的任务?
适合:
- 大量短生命周期的异步任务
- 对执行顺序无严格要求的任务
- CPU密集型或IO密集型均可,但需要不同配置
不适合:
- 需要严格顺序执行的任务
- 长时间阻塞的任务(除非用特殊线程池)
- 对实时性要求极高的任务
8. 源码层面的关键实现
8.1 状态控制设计
线程池用AtomicInteger的ctl字段同时保存:
- workerCount(低29位):当前线程数
- runState(高3位):运行状态(RUNNING, SHUTDOWN等)
这种位运算设计既保证了原子性,又节省了内存。
8.2 任务执行流程
在Worker.runWorker()方法中:
- while循环不断从getTask()获取任务
- getTask()会根据情况:
- 从队列poll(keepAliveTime)获取(可能超时返回null)
- 直接take()阻塞获取(用于核心线程)
- 任务执行前后会调用beforeExecute/afterExecute钩子
8.3 拒绝策略实现
默认的AbortPolicy实现非常简单:
java复制public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException();
}
这也是为什么我们需要自定义策略来处理被拒绝的任务。