Java 线程池
一、为什么要用线程池
不用线程池:
new Thread(runnable).start() → 创建/销毁线程开销大
1000 个任务 = 1000 次创建 + 1000 次销毁
无法控制并发数 → 内存打爆 OOM
用线程池:
线程复用,任务排队,资源可控
1000 个任务 = 最多 corePoolSize 个线程
二、ThreadPoolExecutor 七大参数
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 非核心线程空闲存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
2.1 逐参数详解
corePoolSize --- 核心线程数
含义:
- 线程池始终保留的线程数,即使它们处于空闲状态
- 默认情况下,核心线程不会被回收(除非 allowCoreThreadTimeOut(true))
设置原则:
CPU 密集型: N_CPU + 1
IO 密集型: N_CPU × 2
混合型: N_CPU × 2 ~ 4(压测决定)
不推荐:
Runtime.getRuntime().availableProcessors() × N
↑ 在容器/虚拟化环境中这个值可能不准
maximumPoolSize --- 最大线程数
含义:
- 线程池允许创建的最大线程数
- 只有在队列满了之后才会创建超过 corePoolSize 的线程
注意:
maximumPoolSize 只在队列满了之后才发挥作用。
如果用的是无界队列(如 LinkedBlockingQueue 不设容量),
maximumPoolSize 参数根本不生效 ------ 任务永远先进队列。
keepAliveTime --- 空闲存活时间
含义:
- 当线程数 > corePoolSize 时,多余的空闲线程等待新任务的最长时间
- 超时后,非核心线程被销毁
特殊配置:
executor.allowCoreThreadTimeOut(true)
→ keepAliveTime 对核心线程也生效
→ 核心线程空闲超时也会被回收
workQueue --- 任务队列(关键!)
| 队列类型 |
特性 |
适用场景 |
| SynchronousQueue |
不存储任务,来一个任务必须立刻有线程处理 |
大量短任务,任务数 > 线程数就拒 |
| LinkedBlockingQueue(无界) |
无限排队,maximumPoolSize 不生效 |
任务平稳,不会暴增 |
| LinkedBlockingQueue(有界) |
有限排队,配合 maxPoolSize 使用 |
⭐推荐,资源可控 |
| ArrayBlockingQueue(有界) |
数组实现,固定容量 |
任务数可预测 |
| PriorityBlockingQueue |
按优先级排序 |
有优先级的任务 |
| DelayQueue |
延迟执行 |
定时任务 |
threadFactory --- 线程工厂
ThreadFactory namedFactory = new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("order-pool-" + count.getAndIncrement()); // 有名字好排查
t.setDaemon(false); // 非守护线程(默认)
t.setUncaughtExceptionHandler((thread, e) -> {
log.error("线程 {} 异常退出", thread.getName(), e);
});
return t;
}
};
三、任务执行流程(必须背熟)
execute(task) 被调用:
│
▼
┌ 当前线程数 < corePoolSize?
│ │
│ 是 → new 新线程执行任务(即使有空闲线程)
│
│ 否 → 任务入 workQueue
│ │
│ ┌──入队成功 → 等待空闲线程消费
│ │
│ └──入队失败(队列满)
│ │
│ 当前线程数 < maximumPoolSize?
│ │
│ 是 → new 新线程执行任务
│ 否 → 执行拒绝策略
重要细节:
线程池创建线程的时机:
第 1 ~ corePoolSize 个任务 → 立即创建线程
第 corePoolSize+1 ~ 队列满 → 入队等待
队列满之后的任务 → 创建新线程(最多到 maximumPoolSize)
线程数到 maximumPoolSize 且队列满 → 拒绝
四、拒绝策略
拒绝时机:
① 线程数 = maximumPoolSize 且 workQueue 满
② 线程池已 shutdown,还往里面提交任务
四种内置策略
| 策略 |
行为 |
适用 |
| AbortPolicy(默认) |
抛 RejectedExecutionException |
必须感知到拒绝 |
| CallerRunsPolicy |
由提交任务的线程自己执行 |
不想丢任务,可以降速 |
| DiscardPolicy |
静默丢弃,不报错 |
任务允许丢失(如非关键日志) |
| DiscardOldestPolicy |
丢弃队头最老的任务,重试提交 |
优先保证新任务 |
// AbortPolicy --- 默认,抛异常
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, 10, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy()
);
// CallerRunsPolicy --- 谁提交谁自己跑
// 好处:提交线程被占用来执行任务 → 提交速度自然降低
// 适合:不能丢任务的场景
new ThreadPoolExecutor.CallerRunsPolicy();
// DiscardPolicy --- 丢了就丢了,日志都不会打
new ThreadPoolExecutor.DiscardPolicy();
// DiscardOldestPolicy --- 把队头最老的丢了,塞进新任务
new ThreadPoolExecutor.DiscardOldestPolicy();
自定义拒绝策略
// 场景:拒绝时记录日志 + 发告警 + 落库待后续重试
public class AlertRejectedHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.error("线程池已满!poolSize={}, queueSize={}, task={}",
executor.getPoolSize(),
executor.getQueue().size(),
r.toString());
// 发钉钉/飞书告警
alertService.send("线程池拒绝任务");
// 落库,后续定时任务拉取重试
rejectedTaskRepository.save(r);
}
}
五、预定义线程池 --- 为什么都不推荐
// Executors 提供的便捷方法 --- 全都埋了坑
// ❌ newFixedThreadPool --- LinkedBlockingQueue 无界,OOM
Executors.newFixedThreadPool(10);
// ❌ newCachedThreadPool --- SynchronousQueue + Integer.MAX_VALUE 线程数
// 来一个任务没线程就创建,来 10W 个就创建 10W 个线程 → OOM
Executors.newCachedThreadPool();
// ❌ newSingleThreadExecutor --- 同样无界队列
Executors.newSingleThreadExecutor();
// ❌ newScheduledThreadPool --- 最大线程数 Integer.MAX_VALUE
Executors.newScheduledThreadPool(4);
// ✅ 正确做法:全部自己 new ThreadPoolExecutor,显式指定队列容量
new ThreadPoolExecutor(
10, 20, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // ← 必须有界
namedFactory,
new CallerRunsPolicy()
);
六、线程池状态机
ThreadPoolExecutor 用一个 AtomicInteger ctl 同时表示:
高 3 位:线程池状态
低 29 位:工作线程数量
RUNNING(111)
│ shutdown()
▼
SHUTDOWN(000)
│ 队列空 + 线程全部结束
▼
TIDYING(-1)
│ terminated() 回调执行完
▼
TERMINATED(-3)
RUNNING
│ shutdownNow()
▼
STOP(001)
│
▼
TIDYING → TERMINATED
| 状态 |
接受新任务 |
处理队列中任务 |
怎么进入 |
| RUNNING |
✅ |
✅ |
new ThreadPoolExecutor |
| SHUTDOWN |
❌ |
✅ |
shutdown() |
| STOP |
❌ |
❌ |
shutdownNow() |
| TIDYING |
--- |
--- |
过渡态 |
| TERMINATED |
--- |
--- |
terminated() 执行完 |
七、线程池监控
@Component
public class ThreadPoolMonitor {
@Autowired
private ThreadPoolExecutor orderPool;
@Scheduled(fixedDelay = 5000) // 每 5 秒采集一次
public void monitor() {
int coreSize = orderPool.getCorePoolSize();
int maxSize = orderPool.getMaximumPoolSize();
int activeCount = orderPool.getActiveCount();
int poolSize = orderPool.getPoolSize();
int queueSize = orderPool.getQueue().size();
long completed = orderPool.getCompletedTaskCount();
// 告警规则
if (activeCount >= maxSize) {
log.warn("线程池已满!active={}, max={}", activeCount, maxSize);
}
if (queueSize > 500) {
log.warn("队列堆积严重!queueSize={}", queueSize);
}
// 推送到 Prometheus / Grafana / 飞书
metrics.push("thread_pool_active", activeCount);
metrics.push("thread_pool_queue", queueSize);
}
}
关键监控指标:
1. activeCount / maximumPoolSize → 线程池负载率,> 80% 告警
2. queueSize → 队列积压,> 容量 80% 告警
3. rejectedCount → 拒绝次数,有任何拒绝立刻告警
4. completedTaskCount → 总完成量,看吞吐趋势
5. poolSize → 当前线程数,观察是否需要扩容
八、线程池配置实战
8.1 Spring Boot 配置
@Configuration
public class ThreadPoolConfig {
/**
* 订单处理线程池 --- IO 密集型
*/
@Bean("orderExecutor")
public ThreadPoolExecutor orderExecutor() {
return new ThreadPoolExecutor(
10, // 核心 10
20, // 最大 20
120, TimeUnit.SECONDS, // 空闲 2 分钟回收
new LinkedBlockingQueue<>(500), // 队列 500
new NamedThreadFactory("order"), // 线程命名
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时降速
);
}
/**
* 报表计算线程池 --- CPU 密集型
*/
@Bean("reportExecutor")
public ThreadPoolExecutor reportExecutor() {
return new ThreadPoolExecutor(
4, // CPU 核心数
8,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("report"),
new ThreadPoolExecutor.AbortPolicy() // 报表不能丢
);
}
}
8.2 动态调整
// 运行时动态调整(配 Apollo / Nacos 配置中心实时生效)
@Component
public class DynamicThreadPool {
@Resource(name = "orderExecutor")
private ThreadPoolExecutor orderPool;
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent event) {
if (event.isChanged("order.pool.core")) {
int newCore = event.getInt("order.pool.core", 10);
orderPool.setCorePoolSize(newCore);
log.info("核心线程数调整: {} → {}", orderPool.getCorePoolSize(), newCore);
}
if (event.isChanged("order.pool.max")) {
orderPool.setMaximumPoolSize(event.getInt("order.pool.max", 20));
}
}
}
8.3 优雅关闭
@Component
public class GracefulShutdown implements DisposableBean {
@Resource(name = "orderExecutor")
private ThreadPoolExecutor orderPool;
@Override
public void destroy() {
log.info("开始关闭线程池...");
// 第一步:停止接受新任务
orderPool.shutdown();
try {
// 第二步:等待已提交任务完成(最多等 60 秒)
if (!orderPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 第三步:超时了就强制中断
List<Runnable> undone = orderPool.shutdownNow();
log.warn("强制关闭,未完成任务数: {}", undone.size());
// 第四步:再等 10 秒
if (!orderPool.awaitTermination(10, TimeUnit.SECONDS)) {
log.error("线程池拒绝关闭!");
}
}
} catch (InterruptedException e) {
orderPool.shutdownNow();
Thread.currentThread().interrupt();
}
log.info("线程池已关闭");
}
}
8.4 在异步任务中传递上下文
// 场景:异步任务需要用到当前请求的 tenant_id、user_id
// 问题:异步线程的 ThreadLocal 是空的
// 解决:用装饰器包装任务
public class ContextDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 从主线程捕获上下文
String tenantId = TenantContext.getTenantId();
Long userId = UserContext.getUserId();
String traceId = MDC.get("traceId");
return () -> {
try {
// 设置到异步线程
TenantContext.setTenantId(tenantId);
UserContext.setUserId(userId);
MDC.put("traceId", traceId);
runnable.run();
} finally {
// 清理
TenantContext.clear();
UserContext.clear();
MDC.clear();
}
};
}
}
// 配置中设置
executor.setTaskDecorator(new ContextDecorator());
九、ThreadPoolExecutor 与 ForkJoinPool
|
ThreadPoolExecutor |
ForkJoinPool |
| 设计目标 |
独立任务并发执行 |
可拆分的递归任务 |
| 核心机制 |
阻塞队列 + 线程池 |
工作窃取(Work Stealing) |
| 适用 |
Web 请求、RPC 调用 |
并行计算、递归算法 |
| Java 默认 |
--- |
parallelStream() / CompletableFuture 默认 |
// ForkJoinPool 的工作窃取原理:
//
// Thread-1 队列: [T1, T2, T3, T4] ← 任务多
// Thread-2 队列: [T5] ← 快做完了
// Thread-2 从 Thread-1 的队尾偷一个 → 负载均衡
//
// 这就是为什么 CompletableFuture.supplyAsync() 默认比 newFixedThreadPool 快
十、常见问题排查
问题 1:任务不执行,也不报错
→ 检查:队列是不是无界?任务全在队列里等,线程数没超过 corePoolSize
→ 解决:换成有界队列
问题 2:线程数一直涨到 maxPoolSize
→ 检查:队列是不是太小?核心线程来不及消费
→ 解决:增大队列容量 或 增大 corePoolSize
问题 3:OOM: unable to create new native thread
→ 检查:是不是用了无界队列 + maximumPoolSize 太大?
→ 解决:减小 maximumPoolSize,改用有界队列
问题 4:线程池 shutdown 后线程不退出
→ 检查:队列里的任务是不是阻塞在 IO 上不响应中断?
→ 解决:任务的 IO 操作设置超时时间
问题 5:任务被吞掉,日志没有任何记录
→ 检查:submit() 返回的 Future 是不是没调 get()?
→ 解决:用 execute() 替代 submit(),或调 Future.get()
十一、总结
线程池配置口诀:
核 心 数 --- CPU 密集 +1,IO 密集 ×2
队 列 长 --- 必须设上限,防止 OOM
拒 绝 策 --- 关键业务 Abort,非关键 CallerRuns
命 名 线 程 --- 出问题好排查
监 控 告 警 --- 线程数、队列长度、拒绝次数 三指标
优 雅 关 闭 --- shutdown → awaitTermination → shutdownNow