跳到主要内容

线程池

线程池是并发编程中最重要的组件之一。它通过复用线程、控制并发数量、管理任务队列,有效解决了频繁创建销毁线程带来的性能问题。理解线程池的原理和正确配置方式,对于构建高性能并发应用至关重要。

为什么需要线程池?

线程创建的开销

每个 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("存储队列已满,任务丢失");
}
}
}

任务执行流程

理解线程池的任务执行流程对于正确配置参数至关重要。官方文档对执行逻辑有明确的描述:

核心逻辑详解

  1. 优先创建核心线程:当提交新任务时,如果当前运行的线程数少于 corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新线程来处理这个任务。这个设计确保了线程池在启动阶段能够快速达到核心线程数。

  2. 队列优先于创建线程:当线程数达到 corePoolSize 后,新任务会被放入队列等待,而不是立即创建新线程。只有当队列已满且线程数小于 maximumPoolSize 时,才会创建非核心线程。

  3. 队列满时才扩容:这个设计意味着,如果使用无界队列(如 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() 钩子方法
TERMINATEDterminated() 执行完成,线程池完全终止

状态转换是不可逆的:RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED。

shutdown - 平滑关闭

executor.shutdown();

if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}

shutdown() 方法会:

  1. 将线程池状态设置为 SHUTDOWN
  2. 不再接受新任务(提交新任务会触发拒绝策略)
  3. 等待已提交的任务(包括队列中的任务)执行完成

注意shutdown() 方法不会等待任务执行完成,它会立即返回。如果需要等待,必须使用 awaitTermination() 方法。

shutdownNow - 立即关闭

List<Runnable> unfinishedTasks = executor.shutdownNow();

shutdownNow() 方法会:

  1. 将线程池状态设置为 STOP
  2. 不再接受新任务
  3. 尝试中断正在执行的任务(通过调用 Thread.interrupt()
  4. 返回队列中等待的任务列表

重要提示:中断正在执行的任务依赖于任务代码正确处理中断。如果任务忽略了中断请求,任务可能会继续运行。官方文档明确指出,这是一种"尽力而为"的尝试,没有任何保证。

最佳实践 - 两阶段关闭

官方文档推荐的两阶段关闭方式:

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();
}
}
}

这种方式的优点:

  1. 首先尝试平滑关闭,让正常任务有机会完成
  2. 如果超时,再尝试强制关闭
  3. 正确处理中断,避免丢失中断信号

使用 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 操作,可以使用以下公式估算:

N=Ncpu×(1+WC)N = N_{cpu} \times (1 + \frac{W}{C})

其中:

  • NN 是最优线程数
  • NcpuN_{cpu} 是 CPU 核心数
  • WW 是等待时间(IO、锁等待等)
  • CC 是计算时间

例如,如果 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() 强制关闭。正确处理中断,避免资源泄漏。

最佳实践

  1. 根据任务类型配置线程数:CPU 密集型接近核心数,IO 密集型可以更多
  2. 使用有界队列防止内存溢出
  3. 设置有意义的线程名称便于排查问题
  4. 实现自定义拒绝策略处理过载情况
  5. 通过压测验证配置效果

避坑指南

  1. 避免使用 Executors 工厂方法创建线程池
  2. 避免使用无界队列
  3. 避免忽略拒绝策略
  4. 避免线程数设置过大

线程池配置没有万能公式,需要在理解原理的基础上,根据实际业务场景和系统资源进行调优。