线程池解决什么问题
如果每来一个任务都创建一个线程:
new Thread(task).start();
会有三个问题:
- 创建和销毁线程有成本。
- 线程数量不可控,容易耗尽内存。
- 大量线程会导致频繁上下文切换,反而降低吞吐量。
线程池的核心作用是:用有限的线程处理大量任务,并通过队列和拒绝策略控制系统压力。
它不仅是“复用线程”的工具,更重要的是一种资源治理机制。
线程池基本模型

核心参数
Java 线程池的核心实现是:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
workQueue,
threadFactory,
rejectedExecutionHandler
);
corePoolSize:核心线程数
线程池长期保留的基本线程数量。
默认情况下,即使还没有任务,核心线程也不会立即创建,而是在任务到达时按需创建。
可以提前启动:
executor.prestartAllCoreThreads();
核心线程默认不会因空闲而销毁,除非开启:
executor.allowCoreThreadTimeOut(true);
maximumPoolSize:最大线程数
maximumPoolSize 是线程池允许存在的工作线程上限。超过 corePoolSize 的部分通常被称为非核心线程或临时线程:
最大线程数 = 核心线程 + 临时线程
但它是否真正有效,取决于任务队列。
例如使用无界队列:
new LinkedBlockingQueue<>()
任务几乎总能进入队列,线程池通常不会创建超过 corePoolSize 的线程,因此 maximumPoolSize 基本不起作用。代价是任务堆积不再受队列容量限制,持续过载时可能耗尽内存。
keepAliveTime:临时线程存活时间
当线程数量超过 corePoolSize 后,多出来的线程如果长时间没有任务,就会被销毁。
例如:
60, TimeUnit.SECONDS
表示非核心线程空闲 60 秒后退出。
workQueue:任务队列
用于暂存还没有被执行的任务。
常见队列如下:
- ArrayBlockingQueue:有界数组队列
- 容量固定,内存占用可控
- LinkedBlockingQueue:链表队列
- 不传容量相当于无界队列
- SynchronousQueue
- 不保存任务,而是直接交给工作线程
- 如果没有空闲线程,就尝试创建新线程;达到最大线程数后执行拒绝策略
- PriorityBlockingQueue:优先级队列
- 高优先级任务先执行
- 默认是无界队列
threadFactory:线程工厂
负责创建线程。
生产环境至少应该为线程设置有意义的名字:
AtomicInteger threadNumber = new AtomicInteger();
ThreadFactory factory = runnable -> {
Thread thread = new Thread(runnable);
thread.setName("order-query-pool-" + threadNumber.incrementAndGet());
thread.setUncaughtExceptionHandler((t, e) ->
log.error("线程执行异常, thread={}", t.getName(), e)
);
return thread;
};
线程名非常重要。出现 CPU 飙高、死锁或任务卡住时,可以通过线程名快速定位所属业务。
实际项目中,可以使用 Guava 的 ThreadFactoryBuilder,或者自己维护线程编号。
RejectedExecutionHandler:拒绝策略
Java 线程池的拒绝策略 当线程池已经关闭,或者同时满足以下饱和条件时,线程池无法继续接收任务:
队列已满
+
线程数达到 maximumPoolSize
此时会执行拒绝策略。JDK 提供四种策略:
AbortPolicy:抛出RejectedExecutionException。CallerRunsPolicy:在线程池未关闭时,由提交任务的线程自己执行任务,形成简单的反压;线程池已关闭时则丢弃任务。DiscardPolicy:直接丢弃任务,不抛异常。DiscardOldestPolicy:在线程池未关闭时丢弃队首任务,然后重新尝试提交当前任务;它仍可能再次被拒绝,而且通常不适合不能丢任务的业务。
任务提交后的完整流程
假设线程池参数如下:
corePoolSize = 4
maximumPoolSize = 8
queueCapacity = 100
任务提交过程是:
- 当前工作线程少于 4:创建核心线程执行任务
- 已经有 4 个线程:新任务进入队列
- 队列中已经有 100 个任务:继续创建临时线程
- 线程数量最多增加到 8
- 8 个线程都在忙,并且队列也满了:执行拒绝策略
- 压力下降后,多出来的 4 个临时线程会在空闲超时后销毁
对于这个有界队列配置,在某一时刻能够容纳的已接收任务数量不能简单理解为 8,而是:
正在执行的任务 + 队列中等待的任务
≈ maximumPoolSize + queueCapacity
这只是瞬时容量上限,不代表可持续吞吐量;能否在合理时间内完成还取决于任务耗时、到达速率和线程数配置。
execute 和 submit 区别
execute
executor.execute(() -> doSomething());
特点:
- 只能提交
Runnable - 没有返回值
- 任务抛出的未捕获运行时异常会导致工作线程异常结束,并进入线程的未捕获异常处理机制;线程池通常会补充新的工作线程
- 适合不关心执行结果的任务
submit
Future<String> future = executor.submit(() -> "result");
String result = future.get();
特点:
- 可以提交
Runnable或Callable - 返回
Future - 可以获取结果、取消任务、等待完成
但 submit() 有一个重要陷阱:异常会被封装进 Future。
Future<?> future = executor.submit(() -> {
throw new RuntimeException("执行失败");
});
如果完全不调用:
future.get();
异常可能不会出现在预期的日志中。
因此提交需要关注结果的任务时,应正确处理:
try {
future.get();
} catch (ExecutionException e) {
log.error("任务执行失败", e.getCause());
}