别再乱用 Executors 了!一文掌握 Java 线程池的正确实践
别再乱用 Executors 了!一文掌握 Java 线程池的正确实践
线程池是后端开发中提升性能的利器,但如果用错,它也可能成为耗尽资源的元凶。
在高并发场景下,直接new Thread()不仅会带来巨大的内存与CPU切换开销,还可能导致系统无限制创建线程而崩溃。线程池通过复用线程、控制最大并发数,帮助我们优雅地管理线程资源。
然而,很多同学图方便,随手就是一个 Executors.newFixedThreadPool(10),这很可能为线上故障埋下隐患。今天我们就来聊聊如何正确地使用 Java 线程池。
一、为什么不能滥用 Executors 快捷方法?
Java 的 Executors 类提供了快速创建线程池的工厂方法,但它们各有陷阱:
1. newFixedThreadPool & newSingleThreadExecutor
它们内部使用无界的 LinkedBlockingQueue(长度为 Integer.MAX_VALUE)。
// 源码:队列无界
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
- 风险:如果任务提交速度持续高于处理速度,队列会无限积压,最终撑满内存,导致 OOM。
2. newCachedThreadPool
它允许创建无限数量的线程(最大线程数为 Integer.MAX_VALUE)。
// 源码:最大线程数无界
new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
- 风险:当任务并发突增时,会瞬间创建海量线程,导致 CPU 上下文切换飙升,甚至线程创建失败。
3. newScheduledThreadPool
同样拥有最大线程数为 Integer.MAX_VALUE 的风险。
结论:在生产环境,请手动通过 ThreadPoolExecutor 构造函数来创建线程池,这样才能精确掌控资源。
二、正确构造线程池
ThreadPoolExecutor 共有7个核心参数:
- corePoolSize – 核心线程数(即使空闲也保留)
- maximumPoolSize – 最大线程数
- keepAliveTime – 空闲线程(超过核心数部分)的存活时间
- unit – 时间单位
- workQueue – 任务队列(有界队列是首选)
- threadFactory – 线程工厂(便于自定义线程名)
- handler – 拒绝策略
最佳实践示例
import java.util.concurrent.*;
import com.google.common.util.concurrent.ThreadFactoryBuilder; // Guava 工具
public class ThreadPoolDemo {
public static void main(String[] args) {
// 核心线程数:与CPU密集型/IO密集型任务相关
int corePoolSize = 10;
int maxPoolSize = 20;
long keepAliveTime = 60L;
// 有界队列,容量 100
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
// 自定义线程工厂,命名有意义,方便排查问题
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("biz-pool-%d")
.setDaemon(false)
.build();
// 拒绝策略:CallerRunsPolicy 让提交任务的线程自行执行,减缓提交速度
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i < 200; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务: " + taskId);
try {
Thread.sleep(1000); // 模拟业务处理
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 注意:生产环境中线程池应全局共享,并在应用关闭时优雅终止
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("开始关闭线程池...");
executor.shutdown(); // 不再接受新任务,处理完队列中的任务
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 尝试取消所有执行中的任务
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("线程池已关闭");
}));
}
}
关键点解读:
- 有界队列:
ArrayBlockingQueue或LinkedBlockingQueue指定容量,防止无限制膨胀。 - 线程命名:使用 Guava 的
ThreadFactoryBuilder或自定义ThreadFactory,在日志中能清晰分辨业务线程。 - 拒绝策略:生产环境常用
CallerRunsPolicy(让提交线程自己执行)做自然背压,或AbortPolicy(抛异常,由上层处理),慎用DiscardPolicy(静默丢弃)和DiscardOldestPolicy。 - 优雅停机:通过 shutdown hook 确保任务有序结束,避免数据丢失。
三、线程数到底设多少才合理?
一个简单但不绝对的经验公式:
- CPU 密集型:
核心数 + 1
如加密解密、视频编解码,尽量占满 CPU。 - I/O 密集型:
核心数 * 2或核心数 / (1 - 阻塞系数)(阻塞系数通常取 0.8~0.9 表示任务等待 I/O 的时间比例)
如数据库查询、RPC 调用。
实际调优需结合监控(如线程活跃数、队列积压、任务等待时间)进行压测调整。
四、动态监控与调优
线程池不仅需要创建得当,运行时也要“看得见”。可以通过 ThreadPoolExecutor 自带的 getter 方法暴露指标:
// 定期打印或上报到监控系统
System.out.println("活跃线程: " + executor.getActiveCount());
System.out.println("队列积压: " + executor.getQueue().size());
System.out.println("已完成任务: " + executor.getCompletedTaskCount());
结合 Spring Boot Actuator、Micrometer 或自定义端点,可以实时查看线程池健康状态,防止堆积雪崩。
结语
线程池是并发编程的基石,但它并非“银弹”。远离 Executors 快捷陷阱,坚持手动声明参数、使用有界队列、自定义命名、配置合理拒绝策略,并在生命周期中妥善管理——你的应用才能真正做到高性能与高稳定兼得。