1.线程池怎么使用?
“在 Java 中使用线程池,主要遵循以下规范和步骤:
1. 创建方式:推荐手写 ThreadPoolExecutor 虽然 JDK 提供了 Executors 工厂类(可以快速创建 newFixedThreadPool 等),但在实际生产中,我们严禁直接使用。
-
原因:
Executors创建的线程池,要么是队列长度无限(导致 OOM),要么是最大线程数无限(导致 CPU 爆满)。 -
最佳实践: 直接使用
ThreadPoolExecutor的构造函数来创建,明确指定核心参数,规避资源耗尽风险。
2. 核心参数配置 (最重要的 7 个参数) 我们在创建时需要传入 7 个参数,它们决定了线程池的调度逻辑:
-
corePoolSize(核心线程数): 就算空闲也不会被回收的线程(正式员工)。 -
maximumPoolSize(最大线程数): 线程池能容纳的最大线程数量(正式工 + 临时工)。 -
keepAliveTime(存活时间): 非核心线程空闲多长时间后会被回收。 -
unit(时间单位): 存活时间的单位。 -
workQueue(任务队列): 当核心线程满了,新任务会先放进这个队列排队(候客区)。常用的有ArrayBlockingQueue(有界)和LinkedBlockingQueue。 -
threadFactory(线程工厂): 用于创建线程,一般用来给线程起个名字(方便排查日志)。 -
handler(拒绝策略): 当队列满了且线程数达到最大值时,新任务怎么处理。
3. 提交任务
-
execute():提交 Runnable 任务,没有返回值,无法捕获异常(除非在内部 try-catch)。 -
submit():提交 Callable 或 Runnable 任务,会返回一个Future对象,可以用来获取结果或处理异常。
4. 拒绝策略有哪些? JDK 内置了 4 种,默认是 AbortPolicy。
-
AbortPolicy: 直接抛异常(默认,比较粗暴)。
-
CallerRunsPolicy: 调用者运行。谁提交的任务,谁自己去执行(主线程自己干)。这个策略很好,既不丢任务,又能减缓提交速度。
-
DiscardPolicy: 直接丢弃,不吭声。
-
DiscardOldestPolicy: 丢弃队列里最老的那个任务,尝试把新任务塞进去。”
2.介绍一下线程池的工作原理
- 核心未满直接造 → 核心满了进队列 → 队列满了造非核心 → 全满了走拒绝策略

“Java 线程池(ThreadPoolExecutor)的工作流程遵循一个 ‘核心 → 队列 → 救急 → 拒绝’ 的处理逻辑。当一个新的任务提交到线程池时,处理步骤如下:
第一步:核心线程 (Core Threads)
-
判断当前运行的线程数是否少于
corePoolSize(核心线程数)。 -
如果少于,则直接创建一个新的线程来执行该任务(哪怕其他核心线程是空闲的,也会创建新线程,直到达到核心数)。
第二步:任务队列 (Work Queue)
-
如果当前线程数已经达到了
corePoolSize,任务会被尝试放入workQueue(阻塞队列)中等待。 -
核心线程执行完当前任务后,会去队列里拉取新的任务执行。
第三步:最大线程 (Max Threads)
-
如果队列也塞满了(offer 失败),判断当前运行的线程数是否少于
maximumPoolSize(最大线程数)。 -
如果少于,则创建 非核心线程(也叫救急线程)来立刻执行这个新任务。
-
注意:非核心线程是为了应对突发流量的。
第四步:拒绝策略 (Reject Policy)
-
如果队列满了,且线程数也达到了
maximumPoolSize(忙不过来了),线程池会执行 拒绝策略(RejectedExecutionHandler)。 -
默认的策略是
AbortPolicy,直接抛出异常。”
3.有线程池参数设置的经验吗?
“有的。线程池的参数设置不能一概而论,我通常会根据任务的类型来区分处理,主要分为两类:
1. CPU 密集型任务 (CPU-Intensive)
-
场景: 主要是复杂的计算、加密解密、压缩解压等,CPU 占用率很高。
-
配置策略: 核心线程数 = CPU 核数 + 1。
-
原因: 因为 CPU 一直在全速运转,线程数设多了只会增加 上下文切换 (Context Switch) 的开销,反而降低性能。多加的那个
+1是为了防止线程偶发的缺页中断或其他原因暂停,导致 CPU 空闲,起个候补作用。
2. IO 密集型任务 (IO-Intensive) —— 业务中最常见
-
场景: 主要是读写数据库、Web 请求、文件读写等。线程大部分时间都在 ‘等待’ (Blocking),CPU 并不忙。
-
配置策略: 核心线程数 = CPU 核数 * 2 (或者更精确的公式:$CPU核数 / (1 - 阻塞系数)$)。
-
原因: 因为线程在等待 IO 时不占用 CPU,此时 CPU 是闲着的。为了利用这部分空闲时间,我们需要多开一些线程,让 CPU 去处理其他线程的任务。
3. 队列设置 (Queue)
-
经验: 坚决不使用无界队列(如
newFixedThreadPool默认的LinkedBlockingQueue)。 -
原因: 如果任务处理不过来,队列会无限膨胀,最终导致 OOM (内存溢出)。
-
做法: 我一般使用
ArrayBlockingQueue并设置一个固定的容量(比如 1000),配合合理的拒绝策略。
4.核心线程数设置为0可不可以?
“完全可以设置为 0。 在 Java 的 ThreadPoolExecutor 中,corePoolSize 是允许为 0 的。 但是,设置为 0 后,线程池的工作表现会完全取决于 ‘任务队列 (workQueue)’ 的类型:
情况 1:使用无界/有界阻塞队列 (如 LinkedBlockingQueue)
-
表现: 极度懒惰。
-
流程:
-
第一个任务来了,因为核心线程数是 0,所以不创建线程。
-
任务直接被扔进队列里排队。
-
关键点: 因为当前没有任何活动线程,谁来取任务呢?
-
只要线程池里目前的线程数 < 最大线程数 (
maximumPoolSize),提交任务时会检查‘工作线程数’。如果工作线程数为 0,JVM 会触发创建一个 非核心线程 来干活。 -
但如果队列容量很大,非核心线程可能要等到队列满了才会大规模创建。
-
-
-
后果: 任务可能会在队列里堆积很久,响应会有延迟(冷启动慢)。
情况 2:使用同步队列 (SynchronousQueue)
-
表现: 立即响应 (类似于
CachedThreadPool)。 -
流程:
-
任务来了,尝试放入队列。
-
SynchronousQueue的特点是不存元素,放进去必须马上有人拿走。 -
因为没有核心线程(没人拿),所以
offer操作会失败。 -
线程池发现队列‘满’了(其实是放不进去),且当前线程数 (0) < 最大线程数。
-
立刻创建 一个非核心线程来执行任务。
-
-
后果: 这时候设置 0 没问题,只要有任务来就会创建线程(前提是没超过 max),任务处理很及时。这也是
newCachedThreadPool的默认做法(核心 0,最大无限,同步队列)。”
5.线程池一般是怎么用的?
“在实际开发中,我们使用线程池通常遵循一套标准的 ‘创建 → 提交 → 关闭’ 流程,并且有严格的规范:
1. 创建方式 (Creation) —— 坚决手动 new
-
我们绝不使用 JDK 自带的
Executors工具类(如newFixedThreadPool)。 -
而是通过
new ThreadPoolExecutor(...)的方式手动创建。 -
原因: 手动创建能让我们明确指定核心线程数、队列类型和拒绝策略,避免默认无界队列导致的 OOM 风险,同时也方便给线程自定义命名(比如
order-service-pool-%d),利于排查问题。
2. 提交任务 (Submission) —— 分场景选择
-
不需要返回值时: 使用
execute(Runnable task)。- 它是
Executor接口定义的方法。如果任务抛出异常,线程池会直接打印堆栈信息,如果你没捕获,这个异常可能导致线程终止(然后线程池再补一个新的)。
- 它是
-
需要返回值或处理异常时: 使用
submit(Callable<T> task)。-
它是
ExecutorService接口定义的方法。它会返回一个Future对象。 -
关键点:
submit吃掉的异常不会直接抛出,而是封装在Future里。只有当你调用future.get()时,异常才会抛出来。
-
3. 关闭线程池 (Shutdown) —— 优雅停机
-
业务结束或服务下线时,我们不会直接暴力
shutdownNow()。 -
而是通常使用
shutdown()(不再接收新任务,但把积压的任务跑完)。 -
配合
awaitTermination()来设置一个超时等待时间,防止任务无限期卡死。”
// 1. 定义线程工厂 (为了给线程起个好名字,方便排查 bug)
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("order-pool-%d").build();
// 2. 手动创建线程池 (7 个参数要心里有数)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, // 核心线程数 (Core)
10, // 最大线程数 (Max)
60L, TimeUnit.SECONDS, // 空闲回收时间
new ArrayBlockingQueue<>(1000), // 有界队列 (防 OOM)
namedThreadFactory, // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 (自己跑,不丢任务)
);
// 3. 提交任务
// 方式 A: execute
pool.execute(() -> {
System.out.println("处理订单逻辑...");
});
// 方式 B: submit
Future<String> future = pool.submit(() -> {
return "订单处理完成";
});6.线程池和三个线程同时并发比有什么优势?
| 维度 | 手动创建 (new Thread) | 线程池 (ThreadPool) |
|---|---|---|
| 创建销毁 | 每次都要,开销大 | 复用,开销小 |
| 响应速度 | 慢 (需等待创建) | 快 (拿来即用) |
| 资源控制 | 无限制,易导致 OOM | 有界限,保护系统 |
| 管理功能 | 无 (野生线程) | 有 (监控、拒绝策略) |
7.线程池用了哪些设计模式?
| 设计模式 | 对应组件/接口 | 核心作用 |
|---|---|---|
| 生产者-消费者 | execute() + BlockingQueue + Worker | 解耦提交与执行,缓冲压力 |
| 命令模式 | Runnable / Callable | 封装请求,统一执行标准 |
| 策略模式 | RejectedExecutionHandler | 灵活切换拒绝兜底方案 |
| 工厂模式 | ThreadFactory | 统一管理线程属性 (如名称) |
8.线程池中shutdown (),shutdownNow()这两个方法有什么作用?
| 特性 | shutdown() | shutdownNow() |
|---|---|---|
| 状态变化 | RUNNING → SHUTDOWN | RUNNING → STOP |
| 新任务 | 拒绝 | 拒绝 |
| 正在跑的任务 | 继续跑完 | 尝试中断 (interrupt) |
| 队列里的任务 | 继续跑完 | 不再执行,并返回列表 |
| 返回值 | void | List<Runnable> (未执行的任务) |
9.提交给线程池中的任务可以被撤回吗?
| 任务状态 | 调用 cancel(false) | 调用 cancel(true) | 结果 |
|---|---|---|---|
| 在队列中 | 移除任务 | 移除任务 | ✅ 成功 |
| 正在执行 | 不打断,让它跑完 | 发送 interrupt 信号 | ❓ 看任务配不配合 |
| 已完成 | 无效 | 无效 | ❌ 失败 |