线程池
线程池是并发编程中最重要的组件之一。它通过复用线程、控制并发数量、管理任务队列,有效解决了频繁创建销毁线程带来的性能问题。理解线程池的原理和正确配置方式,对于构建高性能并发应用至关重要。
为什么需要线程池?
线程创建的开销
每个 Java 线程都会映射为一个操作系统线程,创建线程需要付出相当可观的代价。
内存开销:每个线程默认分配约 1MB 的栈空间。创建 1000 个线程就需要约 1GB 内存,仅用于栈空间。
系统开销:创建线程涉及系统调用,需要向操作系统注册线程、分配资源。频繁创建销毁线程会导致大量的系统调用开销。
调度开销:线程数量过多时,操作系统需要频繁进行上下文切换。每次切换都需要保存和恢复寄存器状态、更新调度数据结构,这会消耗大量的 CPU 时间。
资源耗尽风险
无限制地创建线程可能导致系统资源耗尽。当线程数量超过系统承载能力时,不仅会导致应用程序响应变慢,还可能引发更严重的问题:
- 内存溢出:线程栈空间耗尽可用内存
- CPU 饱和:大量线程竞争 CPU,实际用于业务计算的时间减少
- 系统不稳定:可能触发操作系统的限制机制,导致进程被终止
线程管理的复杂性
手动管理线程的生命周期非常繁琐。你需要考虑:如何等待线程完成?如何处理线程中的异常?如何取消正在执行的任务?如何获取任务的执行结果?这些问题如果处理不当,很容易引入难以发现的 bug。
线程池的解决方案
线程池通过以下机制解决上述问题:
- 线程复用:线程执行完任务后不销毁,继续执行其他任务,避免了频繁创建销毁的开销
- 资源控制:限制同时运行的线程数量,防止资源耗尽
- 任务缓冲:通过队列缓存待执行的任务,平滑处理请求峰值
- 统一管理:提供任务提交、执行、关闭等统一接口,简化编程模型
Executor 框架
JUC 的线程池基于 Executor 框架,其核心接口层次如下:
Executor (顶层接口)
└── ExecutorService (扩展接口,支持任务提交和关闭)
├── ThreadPoolExecutor (标准线程池实现)
└── ScheduledThreadPoolExecutor (支持定时任务)
Executor 接口
public interface Executor {
void execute(Runnable command);
}
Executor 是最基础的接口,只定义了一个 execute 方法用于执行任务。
ExecutorService 接口
public interface ExecutorService extends Executor {
void shutdown(); // 平滑关闭
List<Runnable> shutdownNow(); // 立即关闭
boolean isShutdown(); // 是否已关闭
boolean isTerminated(); // 是否已终止
<T> Future<T> submit(Callable<T> task); // 提交有返回值的任务
Future<?> submit(Runnable task); // 提交无返回值的任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks); // 批量执行
}
ExecutorService 扩展了 Executor,提供了更丰富的功能。
ThreadPoolExecutor 核心参数
ThreadPoolExecutor 是线程池的核心实现类,理解其构造参数是正确使用线程池的关键。
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数详解
corePoolSize - 核心线程数
核心线程是线程池中长期保持的线程数量,即使空闲也不会被回收(除非设置了 allowCoreThreadTimeOut)。
当提交新任务时,如果当前线程数小于 corePoolSize,会创建新线程执行任务,即使其他核心线程处于空闲状态。
maximumPoolSize - 最大线程数
线程池允许创建的最大线程数量。当任务队列已满且当前线程数小于 maximumPoolSize 时,会创建非核心线程处理任务。
keepAliveTime - 空闲线程存活时间
非核心线程空闲超过这个时间后会被回收。如果设置了 allowCoreThreadTimeOut(true),核心线程也会被回收。
workQueue - 任务队列
用于缓存等待执行的任务。常见的队列类型:
| 队列类型 | 特点 | 适用场景 |
|---|---|---|
| ArrayBlockingQueue | 有界队列,需指定容量 | 资源有限,防止任务堆积 |
| LinkedBlockingQueue | 可选有界,默认无界 | 任务量可控的场景 |
| SynchronousQueue | 不存储,直接传递 | 高吞吐,任务处理快 |
| PriorityBlockingQueue | 无界优先级队列 | 任务有优先级需求 |
threadFactory - 线程工厂
用于创建新线程,可以自定义线程名称、优先级、是否守护线程等。
ThreadFactory factory = r -> {
Thread t = new Thread(r);
t.setName("my-pool-" + t.getId());
t.setDaemon(false);
return t;
};
handler - 拒绝策略
当线程池已满(达到 maximumPoolSize 且队列已满)时,新任务的处理策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 抛出 RejectedExecutionException | 默认策略,快速失败 |
| CallerRunsPolicy | 由调用者线程执行任务 | 降级处理,减缓提交速度 |
| DiscardPolicy | 直接丢弃任务,不抛异常 | 允许任务丢失 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 | 优先处理新任务 |
拒绝策略详解:
AbortPolicy 是默认策略,它会抛出 RejectedExecutionException 运行时异常。这种"快速失败"的方式可以让调用者立即感知到系统过载,便于监控和告警。
CallerRunsPolicy 是一种优雅的降级策略。当线程池无法处理新任务时,由提交任务的线程自己来执行。这样做有两个好处:一是任务不会丢失,二是由于调用者线程被占用,提交任务的速度自然会减慢,形成自然的"背压"机制。
DiscardPolicy 会静默丢弃任务,不抛出任何异常。只有在你确实不关心任务是否被执行时才应该使用,否则可能会导致问题难以排查。
DiscardOldestPolicy 会丢弃队列中最老的任务,然后尝试重新提交当前任务。这适用于实时性要求高、新任务比老任务更重要的场景。
自定义拒绝策略:
生产环境中,建议实现自定义的拒绝策略,以便更好地处理过载情况:
public class LogAndStorePolicy implements RejectedExecutionHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final BlockingQueue<Runnable> storeQueue;
public LogAndStorePolicy(BlockingQueue<Runnable> storeQueue) {
this.storeQueue = storeQueue;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
logger.warn("任务被拒绝,当前队列大小: {}, 活动线程: {}",
executor.getQueue().size(), executor.getActiveCount());
// 尝试存储任务以便后续重试
if (!storeQueue.offer(r)) {
logger.error("存储队列已满,任务丢失");
}
}
}
任务执行流程
理解线程池的任务执行流程对于正确配置参数至关重要。官方文档对执行逻辑有明确的描述:
核心逻辑详解:
-
优先创建核心线程:当提交新任务时,如果当前运行的线程数少于
corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来处理这个任务。这个设计确保了线程池在启动阶段能够快速达到核心线程数。 -
队列优先于创建线程:当线程数达到
corePoolSize后,新任务会被放入队列等待,而不是立即创建新线程。只有当队列已满且线程数小于maximumPoolSize时,才会创建非核心线程。 -
队列满时才扩容:这个设计意味着,如果使用无界队列(如
LinkedBlockingQueue无参构造),maximumPoolSize参数将永远不会生效,因为队列永远不会满。
队列类型与线程创建的关系
队列类型的选择直接影响线程池的行为,这是配置线程池时最容易出错的地方。
直接传递队列(SynchronousQueue)
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
60L, TimeUnit.SECONDS, new SynchronousQueue<>());
特点:队列不存储任务,而是直接将任务传递给线程。如果没有空闲线程,且线程数未达到最大值,就会创建新线程。
适用场景:任务处理速度快、需要快速响应的场景。但需要注意,如果任务提交速度持续超过处理速度,可能导致线程数量无限增长。
无界队列(LinkedBlockingQueue 无参构造)
new ThreadPoolExecutor(corePoolSize, corePoolSize,
0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
特点:队列容量无限大,所有超出核心线程数的任务都会排队等待。这意味着 maximumPoolSize 参数完全无效,线程池永远不会创建超过 corePoolSize 的线程。
风险:如果任务提交速度持续超过处理速度,队列会无限增长,最终可能导致内存溢出。这是 Executors.newFixedThreadPool() 和 Executors.newSingleThreadExecutor() 存在的问题。
有界队列(ArrayBlockingQueue)
new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
特点:队列有固定容量,队列满后会创建非核心线程,线程数达到最大值后触发拒绝策略。这是最可控的方式,但需要仔细调整队列大小和最大线程数。
权衡:队列大小和最大线程数需要权衡。使用大队列和小线程池可以最小化 CPU 使用和上下文切换开销,但可能导致吞吐量降低。使用小队列和大线程池可以提高吞吐量,但会增加调度开销。
实例演示
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60L, // 空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10), // 任务队列容量10
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.AbortPolicy() // 默认拒绝策略
);
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()
+ " 执行任务 " + taskId);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("任务 " + taskId + " 已提交," +
"核心线程: " + executor.getCorePoolSize() +
", 最大线程: " + executor.getMaximumPoolSize() +
", 当前线程: " + executor.getPoolSize() +
", 队列大小: " + executor.getQueue().size());
}
executor.shutdown();
}
}
Executors 工厂方法
Executors 类提供了创建常用线程池的静态工厂方法,但这些方法在生产环境中需要谨慎使用。
newFixedThreadPool
ExecutorService executor = Executors.newFixedThreadPool(4);
创建固定线程数的线程池,核心线程数和最大线程数相同,使用无界队列。
问题:使用 LinkedBlockingQueue(无界),任务堆积可能导致内存溢出。
newCachedThreadPool
ExecutorService executor = Executors.newCachedThreadPool();
创建可缓存的线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE,使用 SynchronousQueue。
问题:线程数无上限,高并发时可能创建大量线程导致系统崩溃。
newSingleThreadExecutor
ExecutorService executor = Executors.newSingleThreadExecutor();
创建单线程的线程池,保证任务顺序执行。
问题:使用无界队列,任务堆积可能导致内存溢出。
newScheduledThreadPool
ScheduledExecutorService executor = Executors.newScheduledThreadPool(4);
executor.schedule(() -> System.out.println("延迟3秒执行"), 3, TimeUnit.SECONDS);
executor.scheduleAtFixedRate(() -> System.out.println("每2秒执行一次"),
0, 2, TimeUnit.SECONDS);
executor.scheduleWithFixedDelay(() -> System.out.println("上次执行完2秒后执行"),
0, 2, TimeUnit.SECONDS);
支持定时和周期性任务执行。
问题:最大线程数无限制。
阿里巴巴规范
阿里巴巴 Java 开发手册明确禁止使用 Executors 创建线程池:
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
推荐做法:根据业务需求,手动配置 ThreadPoolExecutor 的各个参数。
线程池监控
ThreadPoolExecutor 提供了多个方法用于监控线程池状态:
public class ThreadPoolMonitor {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10)
);
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
while (!executor.isTerminated()) {
System.out.println("=== 线程池状态 ===");
System.out.println("核心线程数: " + executor.getCorePoolSize());
System.out.println("最大线程数: " + executor.getMaximumPoolSize());
System.out.println("当前线程数: " + executor.getPoolSize());
System.out.println("活动线程数: " + executor.getActiveCount());
System.out.println("已完成任务: " + executor.getCompletedTaskCount());
System.out.println("队列大小: " + executor.getQueue().size());
System.out.println("是否关闭: " + executor.isShutdown());
System.out.println("是否终止: " + executor.isTerminated());
Thread.sleep(1000);
}
}
}
线程池关闭
正确关闭线程池是避免资源泄漏的关键。官方文档提供了一个两阶段关闭的示例,这是关闭线程池的最佳实践。
线程池状态
ThreadPoolExecutor 内部维护了线程池的状态,理解这些状态有助于更好地管理线程池生命周期:
| 状态 | 说明 |
|---|---|
| RUNNING | 正常运行状态,可以接受新任务并处理队列中的任务 |
| SHUTDOWN | 调用 shutdown() 后进入,不再接受新任务,但会处理队列中的任务 |
| STOP | 调用 shutdownNow() 后进入,不再接受新任务,中断正在执行的任务 |
| TIDYING | 所有任务已终止,即将执行 terminated() 钩子方法 |
| TERMINATED | terminated() 执行完成,线程池完全终止 |
状态转换是不可逆的:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED。
shutdown - 平滑关闭
executor.shutdown();
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
shutdown() 方法会:
- 将线程池状态设置为 SHUTDOWN
- 不再接受新任务(提交新任务会触发拒绝策略)
- 等待已提交的任务(包括队列中的任务)执行完成
注意:shutdown() 方法不会等待任务执行完成,它会立即返回。如果需要等待,必须使用 awaitTermination() 方法。
shutdownNow - 立即关闭
List<Runnable> unfinishedTasks = executor.shutdownNow();
shutdownNow() 方法会:
- 将线程池状态设置为 STOP
- 不再接受新任务
- 尝试中断正在执行的任务(通过调用
Thread.interrupt()) - 返回队列中等待的任务列表
重要提示:中断正在执行的任务依赖于任务代码正确处理中断。如果任务忽略了中断请求,任务可能会继续运行。官方文档明确指出,这是一种"尽力而为"的尝试,没有任何保证。
最佳实践 - 两阶段关闭
官方文档推荐的两阶段关闭方式:
public class GracefulShutdown {
private final ExecutorService executor = new ThreadPoolExecutor(
4, 8, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)
);
public void shutdown() {
// 第一阶段:平滑关闭
executor.shutdown();
try {
// 等待一段时间让现有任务完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// 第二阶段:强制关闭
executor.shutdownNow();
// 再次等待,让任务响应中断
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("线程池未能完全终止");
}
}
} catch (InterruptedException e) {
// 如果当前线程被中断,也尝试强制关闭
executor.shutdownNow();
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
}
这种方式的优点:
- 首先尝试平滑关闭,让正常任务有机会完成
- 如果超时,再尝试强制关闭
- 正确处理中断,避免丢失中断信号
使用 ShutdownHook
在应用关闭时自动关闭线程池:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("开始关闭线程池...");
shutdown();
System.out.println("线程池已关闭");
}));
线程池配置建议
线程池配置没有万能公式,需要根据任务类型、系统资源、业务需求综合考虑。以下是指导原则和具体建议。
核心线程数设置
CPU 密集型任务
int corePoolSize = Runtime.getRuntime().availableProcessors() + 1;
CPU 密集型任务主要消耗 CPU 资源进行计算,线程数过多反而会因为频繁的上下文切换而降低性能。
为什么是 CPU 核心数 + 1?因为即使 CPU 密集型任务也可能偶尔发生缺页中断或其他暂停,多一个线程可以确保在这种情况下 CPU 不会闲置。但这个 +1 不是必须的,实际效果差异很小。
IO 密集型任务
int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
IO 密集型任务大部分时间在等待 IO 操作完成,CPU 处于空闲状态。此时可以让更多线程同时运行,当一个线程等待 IO 时,CPU 可以执行其他线程。
为什么是 CPU 核心数的 2 倍?这只是一个经验值,实际需要根据 IO 等待时间占总时间的比例来调整。
混合型任务
如果任务既包含 CPU 计算又包含 IO 操作,可以使用以下公式估算:
其中:
- 是最优线程数
- 是 CPU 核心数
- 是等待时间(IO、锁等待等)
- 是计算时间
例如,如果 CPU 计算时间占 40%,等待时间占 60%,则最优线程数约为 核心数 × (1 + 60/40) = 核心数 × 2.5。
实际调优建议
公式只是理论指导,实际配置需要考虑更多因素:
考虑其他进程:服务器上可能运行着其他服务,不能独占所有 CPU 资源。
考虑线程争用:如果线程之间需要竞争锁或共享资源,增加线程数可能反而降低吞吐量。
考虑响应时间要求:如果对响应时间敏感,可能需要增加线程数以减少排队时间。
压测验证:最终配置需要通过压力测试验证,在真实负载下观察系统表现。
推荐的配置方式
生产环境中,建议将线程池参数配置化,便于动态调整:
@Configuration
public class ThreadPoolConfig {
@Value("${thread-pool.core-size:10}")
private int corePoolSize;
@Value("${thread-pool.max-size:20}")
private int maxPoolSize;
@Value("${thread-pool.queue-capacity:100}")
private int queueCapacity;
@Bean
public ThreadPoolExecutor taskExecutor() {
return new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(queueCapacity),
new CustomThreadFactory("task-"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
常见配置错误
错误一:使用无界队列
// 危险!可能导致内存溢出
new ThreadPoolExecutor(10, 10, 0L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
无界队列会导致任务无限堆积,最终可能耗尽内存。应该使用有界队列。
错误二:忽略拒绝策略
如果不设置拒绝策略或使用默认的 AbortPolicy,当系统过载时任务会被直接拒绝,可能导致数据丢失或用户体验下降。应该根据业务场景选择合适的策略。
错误三:线程数设置过大
盲目增加线程数不仅不会提高性能,反而会因为上下文切换开销而降低吞吐量。应该根据任务类型和系统资源合理设置。
线程池扩展
ThreadPoolExecutor 提供了几个可扩展的钩子方法,可以在任务执行前后插入自定义逻辑。
钩子方法
public class MonitoringThreadPoolExecutor extends ThreadPoolExecutor {
private final Logger logger = LoggerFactory.getLogger(getClass());
public MonitoringThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
logger.debug("任务开始执行: {}, 线程: {}", r, t.getName());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
logger.error("任务执行异常: {}", r, t);
} else {
logger.debug("任务执行完成: {}", r);
}
}
@Override
protected void terminated() {
super.terminated();
logger.info("线程池已终止");
}
}
这三个钩子方法的作用:
beforeExecute:任务执行前调用,可用于设置线程上下文、记录开始时间、初始化 ThreadLocal 等afterExecute:任务执行后调用,无论任务是正常完成还是异常终止都会执行,可用于清理资源、记录耗时、统计指标等terminated:线程池完全终止时调用,可用于释放资源、发送通知等
任务执行时间统计
public class TimedThreadPoolExecutor extends ThreadPoolExecutor {
public TimedThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 使用 Runnable 作为 key 存储开始时间
t.setName("task-start-" + System.nanoTime());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// 计算耗时
long endTime = System.nanoTime();
String name = Thread.currentThread().getName();
if (name.startsWith("task-start-")) {
long startTime = Long.parseLong(name.substring(11));
long duration = (endTime - startTime) / 1_000_000;
System.out.println("任务耗时: " + duration + "ms");
}
}
}
动态调整参数
ThreadPoolExecutor 支持运行时动态调整参数:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)
);
// 动态调整核心线程数
executor.setCorePoolSize(15);
// 动态调整最大线程数
executor.setMaximumPoolSize(30);
// 动态调整空闲时间
executor.setKeepAliveTime(120, TimeUnit.SECONDS);
// 允许核心线程超时
executor.allowCoreThreadTimeOut(true);
// 预启动核心线程
executor.prestartAllCoreThreads();
动态调整参数时需要注意:
- 新的核心线程数如果大于当前线程数,会立即创建新线程
- 新的核心线程数如果小于当前线程数,多余的线程会在下次空闲时被回收
- 新的最大线程数不能小于当前核心线程数
线程工厂自定义
使用自定义线程工厂可以为线程设置有意义的名称、优先级和异常处理器:
public class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public CustomThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + "-thread-" + threadNumber.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
// 设置未捕获异常处理器
t.setUncaughtExceptionHandler((thread, ex) -> {
System.err.println("线程 " + thread.getName() + " 发生未捕获异常: " + ex.getMessage());
ex.printStackTrace();
});
return t;
}
}
// 使用
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 20, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new CustomThreadFactory("order-processor")
);
设置有意义的线程名称对于问题排查非常重要,在日志和线程转储中可以快速定位问题线程。
小结
线程池是 Java 并发编程的基础设施,正确理解和使用线程池对于构建高性能并发应用至关重要。以下是本章的核心要点:
核心概念:
线程池通过线程复用、任务队列和资源控制,解决了频繁创建销毁线程带来的性能问题。理解任务执行流程是正确配置线程池的前提:先创建核心线程,再使用队列,队列满后才创建非核心线程。
参数配置:
corePoolSize:核心线程数,决定了线程池的基本并发能力maximumPoolSize:最大线程数,是应对突发流量的缓冲workQueue:任务队列,队列类型决定了线程池的行为模式rejectedExecutionHandler:拒绝策略,决定了过载时的处理方式
队列选择:
队列类型直接影响线程池行为。无界队列会导致 maximumPoolSize 失效,可能导致内存溢出;有界队列提供了资源控制,但需要配合拒绝策略;直接传递队列适合任务处理快的场景。
关闭方式:
遵循两阶段关闭原则:先调用 shutdown() 尝试平滑关闭,超时后调用 shutdownNow() 强制关闭。正确处理中断,避免资源泄漏。
最佳实践:
- 根据任务类型配置线程数:CPU 密集型接近核心数,IO 密集型可以更多
- 使用有界队列防止内存溢出
- 设置有意义的线程名称便于排查问题
- 实现自定义拒绝策略处理过载情况
- 通过压测验证配置效果
避坑指南:
- 避免使用
Executors工厂方法创建线程池 - 避免使用无界队列
- 避免忽略拒绝策略
- 避免线程数设置过大
线程池配置没有万能公式,需要在理解原理的基础上,根据实际业务场景和系统资源进行调优。