线程池(一):核心概念与基础实战
Java 线程池(一):核心概念与基础实战
什么是线程池?
线程池(Thread Pool)是一种基于池化思想管理线程的工具,在多线程服务器中广泛应用,例如数据库连接池、HttpClient 连接池等。
它的核心思想是预先创建一定数量的线程并保存在池中。当有任务需要执行时,线程池会从空闲线程中取出一个来执行任务。任务执行完毕后,该线程会返还给线程池,而不是被销毁,以便后续任务复用。
为什么要使用线程池?
如果不使用线程池,线程的频繁创建与销毁、以及无限制的线程调度会带来巨大的开销,降低计算机的整体性能。线程池通过维护一组工作线程,避免了这些问题,保证了对系统资源的充分利用。
具体来说,使用线程池可以带来一系列好处:
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因资源调度失衡而降低系统稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更强大的功能:线程池具备可拓展性,允许开发者增加更多功能,例如
ScheduledThreadPoolExecutor就允许任务延期或定期执行。
线程池解决了什么核心问题?
线程池解决的核心是资源管理问题。在并发环境下,系统无法预知任意时刻需要执行多少任务、投入多少资源。这种不确定性将带来以下问题:
- 频繁申请/销毁资源和调度资源,将带来巨大的额外消耗。
- 对资源无限申请缺少抑制手段,容易引发系统资源耗尽的风险。
- 系统无法合理管理内部的资源分布,会降低系统的稳定性。
为解决这个问题,线程池采用了“池化”(Pooling)思想。池化指的是提前准备一些资源,在需要时可以重复使用这些预先准备好的资源。
官方文档参考:
Java ThreadPoolExecutor Docs

线程池的创建
创建线程池主要有三种方式:手动创建、使用 Executors 工具类和集成 Spring 框架。
1. 手动创建 ThreadPoolExecutor
这是最推荐的方式,因为它能让开发者完全掌控线程池的所有参数,从而进行精细化配置。
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, // corePoolSize: 核心线程数
4, // maximumPoolSize: 最大线程数
100L, // keepAliveTime: 线程空闲时间
TimeUnit.MINUTES, // unit: 时间单位
new LinkedBlockingDeque<>(4) // workQueue: 任务队列
);2. 使用 Executors 工具类
Executors 提供了一些静态工厂方法来快速创建线程池,例如:
newFixedThreadPool(): 创建定长线程池。newCachedThreadPool(): 创建可缓存线程池。newSingleThreadExecutor(): 创建单线程化线程池。newScheduledThreadPool(): 创建定时线程池。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);然而,强烈不推荐在生产环境中使用 Executors 的这些方法。
其风险在于默认配置可能导致资源耗尽:
FixedThreadPool和SingleThreadExecutor:- 风险:使用了
LinkedBlockingQueue作为任务队列,且队列容量默认为Integer.MAX_VALUE(约 21 亿)。 - 后果:如果任务生产速度持续快于消费速度,任务会不断堆积在队列中,最终可能耗尽内存,导致 OOM (OutOfMemoryError)。
- 风险:使用了
CachedThreadPool和ScheduledThreadPool:- 风险:允许创建的线程数量上限为
Integer.MAX_VALUE。 - 后果:当任务量激增时,线程池会创建大量线程。每个线程都需要消耗一定的系统资源(如内存)。如果线程数量过多,同样可能耗尽系统资源,导致 OOM。
- 风险:允许创建的线程数量上限为
3. 使用 Spring ThreadPoolTaskExecutor
在 Spring/Spring Boot 项目中,使用 ThreadPoolTaskExecutor 是企业级的最佳实践。它不仅提供了与 ThreadPoolExecutor 一致的参数配置,还能交由 Spring 容器管理,并支持更方便的监控。
配置文件 (application.yml)
# application.yml
custom:
thread-pool:
business:
core-pool-size: 10
max-pool-size: 20
queue-capacity: 100
keep-alive-seconds: 60
async:
core-pool-size: 5
max-pool-size: 10
queue-capacity: 50
keep-alive-seconds: 30配置类
@Configuration
@ConfigurationProperties(prefix = "custom.thread-pool.business")
@Data
public class ThreadPoolProperties {
private int corePoolSize = 10;
private int maxPoolSize = 20;
private int queueCapacity = 100;
private int keepAliveSeconds = 60;
}
@Configuration
public class DynamicExecutorConfig {
@Autowired
private ThreadPoolProperties properties;
@Bean("businessExecutor")
public Executor businessExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(properties.getCorePoolSize());
executor.setMaxPoolSize(properties.getMaxPoolSize());
executor.setQueueCapacity(properties.getQueueCapacity());
executor.setKeepAliveSeconds(properties.getKeepAliveSeconds());
executor.setThreadNamePrefix("business-");
executor.initialize();
return executor;
}
}使用
@Autowired
@Qualifier("businessExecutor")
private Executor executor;
// 异步执行业务逻辑
public void someMethod() {
executor.execute(() -> {
// 业务逻辑
});
}创建方式对比
| 方式 | 推荐度 | 说明 |
|---|---|---|
Executors.newXXX() | ❌ 不推荐 | 存在资源耗尽风险(OOM),不适用于生产环境。 |
手动创建 ThreadPoolExecutor | ✅ 推荐 | 参数可控,能明确线程池行为,适用于所有场景。 |
Spring ThreadPoolTaskExecutor | ✅✅ 强烈推荐 | 企业级最佳实践,易于配置、管理和监控。 |
线程池的核心参数
corePoolSize(必需): 核心线程数。默认情况下,核心线程会一直存活。但如果将allowCoreThreadTimeout设置为true,核心线程也会在空闲超时后被回收。maximumPoolSize(必需): 线程池所能容纳的最大线程数。当活跃线程数达到此值且任务队列已满时,新任务将被拒绝。keepAliveTime(必需): 线程闲置超时时长。当线程数超过corePoolSize时,多余的空闲线程如果超过此时长,就会被回收。unit(必需):keepAliveTime的时间单位,如TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。workQueue(必需): 任务队列,用于存储通过execute()方法提交的Runnable对象。它必须是阻塞队列 (BlockingQueue)。threadFactory(可选): 线程工厂,用于自定义新线程的创建方式(如设置线程名、守护状态等)。handler(可选): 拒绝策略。当线程池和任务队列都满时,用于处理新任务的策略。
线程池工作流程示例
private static final int CORE_POOL_SIZE = 2;
private static final int MAX_POOL_SIZE = 4;
private static final int QUEUE_CAPACITY = 4;
// 创建一个线程池
private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
100L,
TimeUnit.MINUTES,
new LinkedBlockingDeque<>(QUEUE_CAPACITY)
);
public static void main(String[] args) {
// 提交 10 个任务
for (int i = 0; i < 10; i++) {
int taskNumber = i;
threadPoolExecutor.execute(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
});
}
// 任务提交完毕后,平滑关闭线程池
// shutdown() 会等待所有已提交的任务执行完毕,不再接受新任务
threadPoolExecutor.shutdown();
// 如果需要立即停止所有正在执行的任务并关闭线程池,可以使用 shutdownNow()
// List<Runnable> unexecutedTasks = threadPoolExecutor.shutdownNow();
}执行分析:
- 任务 0, 1: 触发创建 2 个核心线程 (
corePoolSize) 来执行。 - 任务 2, 3, 4, 5: 核心线程都在忙,任务被放入容量为 4 的任务队列。
- 任务 6, 7: 队列已满,线程池创建 2 个非核心线程(总线程数达到
maxPoolSize4)来执行。 - 任务 8, 9: 此时,线程池已达到最大线程数
maxPoolSize,且队列已满。新任务将被拒绝策略处理(默认为AbortPolicy,抛出RejectedExecutionException)。

线程池的拒绝策略
当线程池无法处理新任务时,会执行拒绝策略。ThreadPoolExecutor 提供了 4 种预设策略:
AbortPolicy(默认): 丢弃任务并抛出RejectedExecutionException异常。CallerRunsPolicy: 不使用线程池的线程,而是由提交任务的调用线程自己来执行该任务。DiscardPolicy: 直接丢弃任务,不抛出任何异常。DiscardOldestPolicy: 丢弃任务队列中最早的未处理任务,然后重新尝试执行当前任务。
你也可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略,但必须谨慎设计,以避免引入并发问题。
线程池的任务队列
任务队列 (BlockingQueue) 在线程池中扮演着关键角色,它与线程池大小的交互决定了任务的处理方式。
队列与线程的交互规则:
- 如果当前运行的线程数少于
corePoolSize,线程池会创建新线程来执行任务,而不是将任务放入队列。 - 如果当前运行的线程数等于或多于
corePoolSize,线程池会优先将任务放入队列,而不是创建新线程。 - 如果任务无法入队(例如队列已满),线程池会尝试创建新线程来执行任务。
- 如果创建新线程会使总线程数超过
maximumPoolSize,则任务将被拒绝。
常见队列类型:
ArrayBlockingQueue: 基于数组的有界阻塞队列,必须指定容量,按 FIFO (先进先出) 排序。LinkedBlockingQueue: 基于链表的阻塞队列。如果未指定容量,默认为Integer.MAX_VALUE,相当于一个无界队列。SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作,反之亦然。它会强制线程池为每个新任务创建新线程(直到maximumPoolSize),因此常用于需要快速响应的场景。PriorityBlockingQueue: 支持优先级的无界阻塞队列。任务的执行顺序由其优先级决定。DelayQueue: 无界的延迟队列,队列中的元素只有在其指定的延迟时间到了之后才能被取出。LinkedBlockingDeque: 基于链表的双端阻塞队列,既可以 FIFO 也可以 FILO (后进先出)。LinkedTransferQueue: 一个高性能的无界队列,融合了ConcurrentLinkedQueue、LinkedBlockingQueue和SynchronousQueue的特点。
注意:使用有界队列有助于防止资源耗尽,但需要仔细调整队列容量和池大小。而使用无界队列时,maximumPoolSize 参数将失效,因为队列永远不会满,也就不会触发创建额外线程的逻辑。
线程池的提交方式
| 方法 | 支持类型 | 返回值 | 能检查状态? | 能获取结果? |
|---|---|---|---|---|
execute() | 仅 Runnable | void | ❌ 不能 | ❌ 不能 |
submit() | Runnable | Future<?> | ✅ 能 (isDone()) | ❌ 无结果 |
submit() | Callable<T> | Future<T> | ✅ 能 (isDone()) | ✅ 能 (get()) |
execute(): 提交一个不需要返回值的任务,无法判断任务是否执行成功。submit(): 提交任务并返回一个Future对象,通过Future可以检查任务状态并获取返回值(如果任务是Callable类型)。get()方法会阻塞,直到任务完成。
线程池的监控
ThreadPoolExecutor 提供了一系列方法来监控其状态和性能:
| 方法 | 含义 |
|---|---|
getActiveCount() | 正在执行任务的线程数量。 |
getCompletedTaskCount() | 已完成的任务数量。 |
getTaskCount() | 已经执行和未执行的任务总数。 |
getPoolSize() | 线程池当前的线程数量。 |
getCorePoolSize() | 线程池的核心线程数量。 |
getMaximumPoolSize() | 线程池的最大线程数量。 |
getLargestPoolSize() | 线程池曾经创建过的最大线程数量,可用于判断线程池是否达到过峰值。 |