《Java 并发编程的艺术》| 书摘・第五辑
摘要:本篇文章围绕 Java 并发编程中的线程池核心内容,梳理了线程池的核心特性,还介绍了 Executor 工具包下的线程池,最后介绍了ScheduledThreadPoolExecutor线程池。


第9章 Java 中的线程池

核心定位
Java 线程池是应用最广泛的并发框架,是处理异步和并发任务的基础工具。它的核心价值是通过池化复用线程,解决了线程频繁创建销毁的性能问题,同时提升了系统的可管理性。
三大核心优势
-
降低资源消耗:线程池会复用已创建的线程,避免了线程创建和销毁带来的系统开销,减少了内存与 CPU 资源的浪费。
-
提高响应速度:任务到达时可直接使用池中的"热备"线程执行,无需等待线程创建,从而缩短任务响应延迟。
-
便于管理:线程是稀缺资源,无限制创建会导致资源耗尽和系统不稳定。线程池可通过参数控制线程数量,实现统一分配、调优和监控,保障系统稳定。
9.2.1 线程池的创建
线程池的参数有哪些?
线程池的构造函数有7个参数:

-
corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。
-
maximumPoolSize:限制了线程池能创建的最大线程总数(包括核心线程和非核心线程),当
corePoolSize已满 并且 尝试将新任务加入阻塞队列失败(即队列已满)并且 当前线程数 <maximumPoolSize,就会创建新线程执行此任务。 -
keepAliveTime:临时线程(超过 corePoolSize 的线程)在空闲状态下的最大存活时间。当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。
-
unit:就是keepAliveTime时间的单位。
-
workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。
-
threadFactory:线程工厂。可以用来给线程取名字等。
-
handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。

这张图是书中对
ThreadPoolExecutor核心执行逻辑的梳理,完整展示了任务提交后的处理路径。
流程核心逻辑
-
提交任务:外部线程向线程池提交一个新的
Runnable任务,触发execute()方法。 -
判断核心线程池是否已满
-
否:直接创建新的核心线程执行该任务。
-
是:进入下一步判断。
-
-
判断任务队列是否已满
-
否:将任务存入阻塞队列等待空闲线程执行。
-
是:进入下一步判断。
-
-
判断线程池是否已达最大线程数
-
否:创建新的非核心线程执行该任务。
-
是:触发拒绝策略。
-
-
执行拒绝策略:当线程池和队列都满时,会按照预设的拒绝策略处理无法执行的任务,常见策略包括抛出异常、直接丢弃、丢弃队列最早任务、让调用者自行执行等。
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务 (执行此步骤需获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新线程来处理任务(执行此步骤需获取全局锁)。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,会按照预设的拒绝策略处理无法执行的任务。
为什么第一步需要全局锁?
当线程池还没达到corePoolSize时,每创建一个新的核心线程,都需要保证线程数统计的准确性。
-
比如线程池的
corePoolSize=5,此时已有 3 个核心线程,同时有 10 个任务并发提交。 -
如果不加全局锁,可能会出现 "多个任务同时判断线程数 < 5,然后同时创建线程" 的情况,最终创建出远超过 5 个核心线程的结果,违背
corePoolSize的设计初衷。 -
因此,创建新核心线程(第一步)必须加全局锁(比如
ReentrantLock),确保同一时间只有一个任务能创建核心线程,保证线程数的精确控制。
采取上述步骤的总体设计思路,是为了在执行 execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2, 而步骤2不需要获取全局锁。
把第二步(入队列)设计在第三步(创建非核心线程)之前,核心目的就是为了最大化规避全局锁,同时兼顾资源效率,这是 ThreadPoolExecutor 设计中最关键的优化思路之一。

1.ArrayBlockingQueue
它是基于数组实现的阻塞队列,必须在初始化时指定固定容量,属于严格的有界队列,遵循先进先出(FIFO)的排序规则。核心特点是读写操作共用同一把锁,因此并发性能稳定但吞吐量处于中等水平,不会出现突发的性能波动。适合对任务数量有明确上限、需要严格控制队列长度,避免任务无限堆积的场景。
2.LinkedBlockingQueue
基于链表实现,默认容量为Integer.MAX_VALUE(日常使用中可视为无界队列),也可手动指定容量变为有界,同样遵循先进先出(FIFO)规则。它的核心优势是读写操作使用分离锁(读锁和写锁分开),因此吞吐量通常高于 ArrayBlockingQueue。是Executors.newFixedThreadPool()的默认队列,适合任务量较大、希望缓冲更多任务,且对队列容量没有严格限制的场景。
3.SynchronousQueue
这是一种特殊的队列,没有实际的存储结构、容量为 0,不存在 "缓冲任务" 的概念,也没有固定的排序规则。它的核心特性是 "直接移交":插入任务的操作必须等待有线程来取走任务,否则会阻塞;任务会直接从生产者线程移交到消费者线程,无任何缓冲开销。是Executors.newCachedThreadPool()的默认队列,适合任务处理速度快、任务量波动大(高峰期任务多、低峰期几乎无任务)的场景。
4.PriorityBlockingQueue
基于堆结构实现,属于无界队列,不遵循先进先出规则,而是按照自定义优先级排序执行任务(需要任务实现Comparable接口来定义优先级)。核心特点是能优先处理高优先级任务,哪怕高优先级任务提交较晚,也会插队执行。适合任务有明确优先级高低区分、需要优先处理核心 / 重要任务的场景(比如订单处理中,VIP 用户订单优先于普通用户订单)。
线程池工作队列满了有哪些拒接策略?
当线程池的任务队列满了之后,线程池会执行指定的拒绝策略来应对,常用的四种拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy,此外,还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。
四种预置的拒绝策略:
-
AbortPolicy,直接抛出一个任务被线程池拒绝的异常。(默认拒绝策略)
-
DiscardPolicy,不做任何处理,静默拒绝提交的任务。
-
DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
-
CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
-
自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。
9.2.2 向线程池提交任务


一、execute() 方法
execute() 是线程池最基础的任务提交方法,专门用于提交不需要返回值的任务。
-
任务类型:仅支持
Runnable类型的任务,这类任务只执行逻辑,不返回结果。 -
核心特点:提交后无法直接判断任务是否执行成功,也无法获取任务的执行结果。
二、submit() 方法
submit() 是增强型的任务提交方法,用于提交需要返回值的任务。
-
任务类型:支持
Runnable和Callable两种类型的任务。Callable任务可以通过call()方法返回结果。 -
核心特点:
-
提交后会返回一个
Future对象,通过它可以判断任务是否执行成功。 -
调用
future.get()会阻塞当前线程,直到任务完成并返回结果;带超时参数的get(long timeout, TimeUnit unit)会在超时后立即返回,此时任务可能未执行完毕。 -
可以捕获任务执行中的异常(如中断异常、任务执行异常),便于错误处理。
-
Future<Object> future = executor.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
// 执行有返回值的任务逻辑
return "任务执行结果";
}});
try {Object result = future.get();
// 阻塞等待结果
} catch (InterruptedException e) {
// 处理线程中断异常
} catch (ExecutionException e) {
// 处理任务执行异常
} finally {
// 关闭线程池
executor.shutdown();
}

9.2.3 关闭线程池

shutdown() 方法(温和关闭)
-
状态变更:将线程池的状态设置为
SHUTDOWN。 -
中断逻辑:仅中断所有没有正在执行任务的空闲线程。
-
任务处理:已提交的任务(包括正在执行和队列中等待的)会继续执行完毕。
-
返回值:无返回值。
-
适用场景:希望等待所有已提交任务正常完成后再关闭线程池的场景,比如系统优雅停机、批处理任务结束后清理资源。
shutdownNow() 方法(强制关闭)
-
状态变更:将线程池的状态设置为
STOP。 -
中断逻辑:尝试停止所有正在执行或暂停任务的线程(包括正在工作的线程)。
-
任务处理:不再处理队列中等待的任务,并将这些任务组成列表返回。
-
返回值:返回等待执行的任务列表,便于后续处理。
9.2.4 合理地配置线程池

按任务性质的核心配置策略
1.CPU 密集型任务
-
特点:任务持续占用 CPU 进行计算,几乎不涉及 IO 等待。
-
配置建议:线程数应尽可能小,推荐公式为
CPU核心数 + 1。 -
原理:过多线程会导致频繁的上下文切换,反而降低 CPU 利用率。
2.IO 密集型任务
-
特点:任务大部分时间在等待 IO 操作(如数据库查询、网络请求),CPU 处于空闲状态。
-
配置建议:线程数应尽可能多,推荐公式为
2 * CPU核心数。 -
原理:更多线程可以在 IO 等待时切换执行,充分利用 CPU 资源。
3.混合型任务
-
特点:任务同时包含 CPU 计算和 IO 等待。
-
配置建议:如果可以拆分,将其拆分为独立的 CPU 密集型任务和 IO 密集型任务。
-
原理:拆分后两个任务可并行执行,能显著提升整体吞吐量。
第10章 Executor 工具包
ThreadPoolExecutor详解
FixedThreadPool



SingleThreadExecutor


CachedThreadPool


ScheduledThreadPoolExecutor


使用ScheduledThreadPoolExecutor实现延时任务
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j(topic = "c.DelayTaskDemo")
public class DelayTaskDemo {
public static void main(String[] args) {
// 1. 创建定时任务线程池(核心线程数=1)
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(1);
log.info("程序启动,开始提交延迟任务...");
// 2. 提交延迟任务(核心方法:schedule)
// 参数说明:
// - 第一个参数:要执行的延迟任务逻辑(Lambda表达式)
// - 第二个参数:延迟时间(这里设置为5秒)
// - 第三个参数:时间单位(这里是秒)
scheduledPool.schedule(() -> {
// 延迟任务的核心业务逻辑
try {
log.info("延迟任务开始执行:模拟订单超时关闭");
// 这里写实际业务逻辑,比如:更新订单状态为“已取消”、释放库存等
Thread.sleep(1000); // 模拟业务执行耗时
log.info("延迟任务执行完成:订单已关闭");
} catch (Exception e) {
log.error("延迟任务执行失败", e);
}
}, 5, TimeUnit.SECONDS);
}
}
恭喜你学习完本节内容!✿
更多推荐



所有评论(0)