目录
一、Java 线程池的继承结构
常用的是哪种?
ForkJoinPool 什么场合用?
二、线程池是什么(为什么要用)
二、核心组件(ThreadPoolExecutor 的七大参数)
① corePoolSize(核心线程数)
② maximumPoolSize(最大线程数)
③ keepAliveTime(空闲存活时间)
④ TimeUnit(时间单位)
⑤ workQueue(任务队列)
⑥ threadFactory(线程工厂)
⑦ RejectedExecutionHandler(拒绝策略)
三、任务执行流程(面试必问)
四、四种快捷创建方式(不推荐!)
五、实战:从零创建一个线程池(手把手)
第一步:建一个线程池
第二步:理解每个任务怎么跑的
第三步:验证线程池状态
六、线程池的五种状态
七、execute() 和 submit() 的区别
八、如何设置线程池大小(面试高频)
一、阿里手册确实禁止
二、线程池用的是谁的资源?
三、那微服务里线程池到底是谁的?
一句话
九、面试话术汇总
Q1:线程池的七大参数是什么?执行流程是怎样的?
Q2:为什么阿里巴巴禁止使用 Executors 创建线程池?
Q3:核心线程数怎么设置?
Q4:shutdown() 和 shutdownNow() 的区别?
Q5:线程池的拒绝策略有哪些?什么时候用哪种?
Q6:线程池里的线程什么时候被销毁?
Q7: execute 和 submit 哪个好?
Q8:线程池监控怎么做?
Q9:线程池提交任务时,核心线程都在忙、队列也满了,这时是先创建临时线程还是先拒绝?
Q10:subimit 和 execute 异常丢失具体指什么?
面试刁钻题 1:线程池中的线程抛出异常会怎样?这个线程会被销毁吗?
面试刁钻题 2:用 submit 提交,future.get() 阻塞等待时,这个线程是否算在"活跃线程数"里?
面试刁钻题 3:线程池的 prestartAllCoreThreads() 是干什么的?预热有什么用?
面试刁钻题 4:一个线程池的 corePoolSize=0,maximumPoolSize=10,队列是 SynchronousQueue,会怎么样?这和 newCachedThreadPool 有什么不同?
面试刁钻题 5:线程池的队列满了后,新任务是立即创建临时线程执行,还是等队列任务被取走一个再执行?
面试刁钻题 6:线程池什么时候从队列里取任务?
面试刁钻题 7:线程池的线程做完任务后,是直接销毁还是怎么回到池子里?
十、线程池的三个核心问题
10.1 什么时候用
10.2 什么时候别用
10.3 什么时候注意 ThreadLocal
十一、一句话速记
一、Java 线程池的继承结构
整个线程池体系从顶层接口到底层实现,四个层级:
Executor(顶级接口)
│ 只有一个方法:void execute(Runnable command)
│ 简单说:你给我一个任务,我帮你跑
│
└── ExecutorService(子接口)
│ 扩展了三大能力:
│ ① 提交有返回值的任务(submit(Callable))
│ ② 关闭线程池(shutdown/shutdownNow)
│ ③ 批量提交(invokeAll/invokeAny)
│
├── ThreadPoolExecutor(最常用的实现类)
│ 普通阻塞队列线程池,七大参数在这里定义
│ 用来执行大部分日常任务
│
└── ForkJoinPool(分支合并线程池)
工作窃取算法,专治分治任务(把大任务拆小)
Java 8 的 parallelStream 底层就是这个
常用的是哪种?
你 90% 的场景用 ThreadPoolExecutor。
// 最常用的是这个
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)
);
ForkJoinPool 什么场合用?
// 大任务拆小:计算 1 到 100 万的和
// RecursiveTask 或 RecursiveAction
// parallelStream 底层就是 ForkJoinPool
int sum = IntStream.range(1, 1_000_000).parallel().sum();
// ForkJoinPool.commonPool() 默默在干活
和 ThreadPoolExecutor 的核心区别:
| 队列 | 一个公共任务队列 | 每个线程一个自己的队列 |
| 任务获取 | 从公共队列取 | 先拿自己的,自己的空了去偷别人的 |
| 适用场景 | 通用任务执行 | 分治任务(大拆小) |
一般面试只需要记住:Executor → ExecutorService → ThreadPoolExecutor / ForkJoinPool 这个继承结构,用的最多的就是 ThreadPoolExecutor,ForkJoinPool 知道是 parallelStream 的底层就行了。
二、线程池是什么(为什么要用)
线程池就是一个装线程的"池子",里面有若干个已经创建好的线程,等着你来派活。
没有线程池时:
来一个任务 → new 一个线程 → 用完就扔掉(销毁)
→ 频繁创建/销毁线程,性能差
有线程池时:
先创建一个池子,里面放 5 个线程
来一个任务 → 从池子里拿一个空闲线程去执行 → 执行完线程回到池子
→ 线程复用,减少创建销毁开销
为什么需要它:
| 频繁创建/销毁线程,CPU 开销大 | 线程复用,创建一次用多次 |
| 线程数量无限制,可能 OOM | 控制最大线程数,超出的排队 |
| 没法管理线程 | 统一管理:监控线程状态、任务队列 |
| 系统负载不可控 | 任务太多时排队或拒绝 |
二、核心组件(ThreadPoolExecutor 的七大参数)
Java 的线程池核心实现是 ThreadPoolExecutor,它要用到七个参数来构造(面试必背):
public ThreadPoolExecutor(
int corePoolSize, // ① 核心线程数
int maximumPoolSize, // ② 最大线程数
long keepAliveTime, // ③ 空闲线程存活时间
TimeUnit unit, // ④ 时间单位
BlockingQueue<Runnable> workQueue, // ⑤ 任务队列
ThreadFactory threadFactory, // ⑥ 线程工厂
RejectedExecutionHandler handler // ⑦ 拒绝策略
)
① corePoolSize(核心线程数)
常驻线程的数量,即使这些线程闲着也不销毁(除非设置了 allowCoreThreadTimeOut)。
// 假如设 corePoolSize = 5,线程池里永远至少有 5 个线程活着
// 来 3 个任务 → 用其中 3 个线程,剩下 2 个空闲
// 来 10 个任务 → 5 个线程全跑满了
② maximumPoolSize(最大线程数)
线程池最多能有的线程数。当核心线程都忙且队列也满了,就会创建新的临时线程直到这个数。
// 假如 corePoolSize=5, maximumPoolSize=10, 队列容量=100
// 来了 200 个任务 → 5 个核心线程先跑
// 5 个跑满了 → 后面 100 个排队
// 队列也满了 → 再来任务就创建临时线程(最多到 10 个)
// 10 个线程也满了 → 触发拒绝策略
③ keepAliveTime(空闲存活时间)
临时线程空闲了能活多久。超时没接到新任务就销毁。
// 假如 keepAliveTime = 60 秒
// 临时线程干完活后,没事干等 60 秒 → 自动销毁,释放资源
④ TimeUnit(时间单位)
TimeUnit.SECONDS // 秒
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.MINUTES // 分钟
⑤ workQueue(任务队列)
核心线程满了,新任务先排队。排队也满了才创建临时线程。
常见队列:
| ArrayBlockingQueue | 有界队列,容量固定 | 常用,能防止 OOM |
| LinkedBlockingQueue | 可设容量的链表队列 | 默认无界(Integer.MAX_VALUE),小心 OOM |
| SynchronousQueue | 容量为 0,不排队,直接交给线程 | newCachedThreadPool 用这个 |
| PriorityBlockingQueue | 支持优先级排序 | 需要优先处理某类任务时 |
| DelayQueue | 延迟执行 | 定时任务场景 |
⑥ threadFactory(线程工厂)
给线程取名字的。默认名字是 pool-1-thread-1,出问题你根本看不出来这是哪个池子的。
// ✅ 正确做法:自定义名字
ThreadFactory namedFactory = new ThreadFactory() {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("order-service-pool-" + counter.getAndIncrement());
return t;
}
};
⑦ RejectedExecutionHandler(拒绝策略)
线程池忙不过来(线程满了、队列满了)时怎么办。
| AbortPolicy(默认) | 抛 RejectedExecutionException |
| CallerRunsPolicy | 谁提交的任务谁自己执行(在提交者的线程里跑) |
| DiscardPolicy | 静默丢弃,不抛异常 |
| DiscardOldestPolicy | 丢弃队列里最老的那个任务,再试一次 |
// 拒绝策略一般用 CallerRunsPolicy 最安全
// 因为提交者(比如 Tomcat 线程)执行,起到了"反压"作用
new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
// 这样至少任务不会丢,只是慢一点
三、任务执行流程(面试必问)
这是线程池最核心的知识点,七步递进:
提交一个任务
│
▼
① 当前线程数 < corePoolSize?
├── 是 → 创建新线程执行任务
└── 否 → 进入下一步
│
▼
② 尝试入队(workQueue.offer(task))
├── 入队成功 → 任务在队列里等着,线程空闲时取出来执行
└── 入队失败 → 进入下一步(队列满了,比如 ArrayBlockingQueue(100) 满了)
│
▼
③ 当前线程数 < maximumPoolSize?
├── 是 → 创建新临时线程执行任务
└── 否 → 执行拒绝策略 handler.rejectedExecution(task)
简化速记版:
核心线程跑 → 满了去排队 → 队满了扩线程 → 扩满了就拒
四、四种快捷创建方式(不推荐!)
Java 提供了 Executors 工具类,一行代码创建线程池——但阿里巴巴手册明确禁止:
// ① 固定线程数
ExecutorService fixed = Executors.newFixedThreadPool(5);
// 问题:LinkedBlockingQueue 无界(最大 Integer.MAX_VALUE)
// 任务积压 → OOM
// ② 单线程
ExecutorService single = Executors.newSingleThreadExecutor();
// 同上,无界队列
// ③ 缓存线程池
ExecutorService cached = Executors.newCachedThreadPool();
// 问题:maxPoolSize = Integer.MAX_VALUE
// 请求量大时 → 创建无限多线程 → OOM
// ④ 定时任务
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(5);
// 同上,maxPoolSize 无限
阿里巴巴为什么禁止:
| newFixedThreadPool | 无界队列 | 任务积压导致 OOM |
| newCachedThreadPool | 无限线程 | 线程创建太多导致 OOM |
| newSingleThreadExecutor | 无界队列 | 同 fixed |
| newScheduledThreadPool | 无限线程 | 同 cached |
正确做法:自己 new ThreadPoolExecutor 显式传参
五、实战:从零创建一个线程池(手把手)
第一步:建一个线程池
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 1. 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize = 2
4, // maximumPoolSize = 4
60, // keepAliveTime = 60 秒
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3), // 队列容量 = 3
new NamedThreadFactory("order-pool"), // 自定义线程名
new ThreadPoolExecutor.CallerRunsPolicy() // 提交者自己执行
);
// 2. 提交 8 个任务
for (int i = 1; i <= 8; i++) {
int taskId = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()
+ " 正在执行任务 " + taskId);
try {
Thread.sleep(2000); // 模拟任务执行 2 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " 执行完任务 " + taskId);
});
}
// 3. 关闭线程池
executor.shutdown();
}
// 自定义线程工厂:给线程取个好名字
static class NamedThreadFactory implements ThreadFactory {
private final String prefix;
private final AtomicInteger counter = new AtomicInteger(1);
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(prefix + "-" + counter.getAndIncrement());
return t;
}
}
}
第二步:理解每个任务怎么跑的
核心池:2 个线程(order-pool-1, order-pool-2)
队列:3 个位置
最大线程:4 个
提交任务 1 → order-pool-1 执行(没到 corePoolSize,创建新线程)
提交任务 2 → order-pool-2 执行
提交任务 3 → 入队(核心线程满了)
提交任务 4 → 入队
提交任务 5 → 入队
提交任务 6 → 队列满了!创建临时线程 order-pool-3 执行
提交任务 7 → 队列满了!创建临时线程 order-pool-4 执行
提交任务 8 → 线程数到 maximumPoolSize=4 了!触发拒绝策略(CallerRunsPolicy → main 线程自己执行)
最终:order-pool-1, -2, -3, -4 + main 线程 = 5 个线程同时执行
第三步:验证线程池状态
// 提交完任务后,打印线程池状态
System.out.println("=== 线程池状态 ===");
System.out.println("活跃线程数:" + executor.getPoolSize());
System.out.println("核心线程数:" + executor.getCorePoolSize());
System.out.println("队列中任务数:" + executor.getQueue().size());
System.out.println("已完成任务数:" + executor.getCompletedTaskCount());
六、线程池的五种状态
shutdown()
RUNNING ──────────────→ SHUTDOWN
│ (不收新任务,跑完队列里的)
│ │
│ shutdownNow() │
├────────────────────→ STOP
│ (不收新任务,也不跑队列,中断正在跑的)
│ │
│ │ 队列空 + 所有线程终止
│ ▼
│ TIDYING
│ │
│ │ terminated() 钩子执行完
│ ▼
└──────────────────── TERMINATED
executor.shutdown(); // 优雅关闭:等队列跑完
executor.shutdownNow(); // 暴力关闭:中断正在跑的,返回队列里的任务
executor.isShutdown(); // 是否已关闭
executor.isTerminated(); // 是否已完全终止
executor.awaitTermination(10, TimeUnit.SECONDS); // 等待线程池关闭(最多等 10 秒)
面试常问:shutdown 和 shutdownNow 区别?
shutdown:不接受新任务,但会等队列里已有的任务执行完,再关闭。shutdownNow:不接受新任务,正在执行的线程调 interrupt(),队列里的任务直接返回,不执行了。
七、execute() 和 submit() 的区别
// execute:提交 Runnable,没有返回值,异常直接打印
executor.execute(() -> {
System.out.println("没有返回值");
});
// submit:提交 Callable,返回 Future 可以拿结果或异常
Future<String> future = executor.submit(() -> {
return "有返回值";
});
String result = future.get(); // 阻塞等待结果
// 如果 submit 的任务抛异常,future.get() 会抛出
// 而 execute 的异常直接打到控制台,你可能不知道
面试常问:
// execute 异常丢失问题
executor.execute(() -> {
throw new RuntimeException("报错了"); // 控制台打印,没人知道
});
// submit 异常
Future<?> future = executor.submit(() -> {
throw new RuntimeException("报错了");
});
future.get(); // 抛 ExecutionException,你能捕获到
结论:生产环境用 submit,不要用 execute,至少异常不会无声无息丢。
八、如何设置线程池大小(面试高频)
公式:
CPU 密集型任务(大量计算):
线程数 = CPU 核数 + 1
IO 密集型任务(大量网络/磁盘等待):
线程数 = CPU 核数 × 2 或更大
更精确的公式(美团方案):
线程数 = CPU核数 × (1 + 等待时间 / 计算时间)
// 获取 CPU 核数
int cpus = Runtime.getRuntime().availableProcessors();
// CPU 密集型
ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(
cpus + 1, cpus + 1, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000)
);
// IO 密集型(比如调远程服务)
ThreadPoolExecutor ioPool = new ThreadPoolExecutor(
cpus * 2, cpus * 2, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000)
);
为什么 CPU 密集型不能太多:
-
假如 4 核 CPU,开 8 个线程
-
1 个线程在跑计算,其他 7 个在抢 CPU
-
线程切换本身就要消耗 CPU 时间
-
反而比 4 个线程跑得更慢
为什么 IO 密集型可以多:
-
线程在等数据库/远程服务返回时,CPU 是空闲的
-
更多的线程可以让 CPU 在等待期间干别的事
-
不浪费 CPU 时间片
九、面试话术汇总
Q1:线程池的七大参数是什么?执行流程是怎样的?
话术:七大参数是 corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、workQueue、threadFactory、RejectedExecutionHandler。执行流程是:先看核心线程有没有满,没满就创建核心线程;满了就入队;队满了就创建临时线程直到最大线程数;再满就执行拒绝策略。
Q2:为什么阿里巴巴禁止使用 Executors 创建线程池?
话术:三个原因。newFixedThreadPool 和 newSingleThreadExecutor 用了无界的 LinkedBlockingQueue,队列容量是 Integer.MAX_VALUE,任务积压会导致 OOM。newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE,高并发下会创建无限线程也导致 OOM。而且默认的线程名没有业务含义,出问题不好排查。所以推荐自己 new ThreadPoolExecutor 显式传参。
Q3:核心线程数怎么设置?
话术:分 CPU 密集型和 IO 密集型。CPU 密集型设 CPU 核数+1,避免线程切换浪费 CPU。IO 密集型设 CPU 核数×2 或更大,因为线程在等待 IO 时 CPU 是空闲的。更精确的公式是:线程数 = CPU核数 × (1 + 等待时间/计算时间),但实际还是需要通过压测来验证。
Q4:shutdown() 和 shutdownNow() 的区别?
话术:shutdown 是优雅关闭,不再接收新任务但会执行完队列里已有的任务。shutdownNow 是暴力关闭,不再接收新任务,正在执行的任务调 interrupt() 中断,队列里还没执行的任务直接返回。生产环境优先用 shutdown,除非需要快速释放资源。
Q5:线程池的拒绝策略有哪些?什么时候用哪种?
话术:四种。AbortPolicy(默认,抛异常)、CallerRunsPolicy(谁提交谁执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢最老任务再试)。生产环境最推荐 CallerRunsPolicy,因为让提交者自己执行起到了反压作用,不会丢任务,只是请求变慢。AbortPolicy 如果调用方没处理好异常,任务就丢了。
Q6:线程池里的线程什么时候被销毁?
话术:核心线程默认不销毁,除非设置了 allowCoreThreadTimeOut(true)。非核心线程(超过 corePoolSize 创建的那些)在空闲时间超过 keepAliveTime 后自动销毁。所以如果任务量降下来了,临时线程会自动回收,释放资源。
Q7: execute 和 submit 哪个好?
话术:生产环境用 submit。execute 提交的任务抛异常只会打印到控制台,不会通知调用方,容易造成异常静默丢失。submit 返回 Future,调用 future.get() 会抛 ExecutionException,能捕获并处理异常。而且 submit 支持 Callable 有返回值。
Q8:线程池监控怎么做?
话术:通过 ThreadPoolExecutor 自带的 getPoolSize()、getActiveCount()、getQueue().size()、getCompletedTaskCount() 等方法获取实时状态。还可以通过 beforeExecute 和 afterExecute 钩子方法做埋点,记录每个任务的执行耗时。生产环境一般对接 Prometheus + Grafana 做可视化监控。
Q9:线程池提交任务时,核心线程都在忙、队列也满了,这时是先创建临时线程还是先拒绝?
话术:先创建临时线程。完整流程是:① 核心线程 < corePoolSize → 创建;② 尝试入队;③ 入队失败(队列满了)且线程数 < maximumPoolSize → 创建临时线程;④ 线程数已到 maximumPoolSize → 执行拒绝策略。
Q10:subimit 和 execute 异常丢失具体指什么?
话术:execute 的任务如果抛了 RuntimeException,线程池会捕获后打印到 System.err,但调用方完全不知道,异常静默丢失。submit 的任务抛异常,异常会被包装在 Future 里,调用 future.get() 时会抛 ExecutionException,调用方可以 catch 到并处理。所以线上代码建议用 submit + 处理 future。
面试刁钻题 1:线程池中的线程抛出异常会怎样?这个线程会被销毁吗?
话术:会。Worker 线程在执行任务时如果抛异常,runWorker 方法会捕获到,执行 afterExecute 钩子,然后线程退出。线程池会判断当前线程数是否小于核心线程数,如果是就创建一个新 Worker 补充进来。所以线程池并不会因为这个线程挂了就一直少一个线程,它会自动补充到 corePoolSize。
面试刁钻题 2:用 submit 提交,future.get() 阻塞等待时,这个线程是否算在"活跃线程数"里?
话术:算的。future.get() 阻塞的是调用 get() 的那个线程(通常是业务主线程),不是线程池里的 Worker 线程。Worker 线程在任务执行完后已经回到线程池了。get() 阻塞的是外面的线程,不是池子里的线程,池子里的活跃线程数已经是空闲状态了。
面试刁钻题 3:线程池的 prestartAllCoreThreads() 是干什么的?预热有什么用?
话术:默认情况下核心线程是懒加载的,来第一个任务才创建。prestartAllCoreThreads() 会提前创建所有核心线程。预热的作用是避免第一个请求因为线程创建而变慢,常用于对延迟敏感的系统。比如网关服务,启动时先预热好线程,第一个请求来的时候线程已经存在了。
面试刁钻题 4:一个线程池的 corePoolSize=0,maximumPoolSize=10,队列是 SynchronousQueue,会怎么样?这和 newCachedThreadPool 有什么不同?
话术:SynchronousQueue 队列容量为 0,不存储任务,必须直接交给线程。因为 corePoolSize=0,来任务时会先尝试创建临时线程。这与 newCachedThreadPool 行为类似,区别是 newCachedThreadPool 的 keepAliveTime=60s(临时线程存活 60 秒),而如果这里自己设 corePoolSize=0 + SynchronousQueue + keepAliveTime=0,那临时线程干完活就立刻销毁了,几乎等于来一个任务就 new 一个线程、用完就销毁,性能极差。
面试刁钻题 5:线程池的队列满了后,新任务是立即创建临时线程执行,还是等队列任务被取走一个再执行?
话术:立即执行。offer() 入队失败返回 false,不会等队列有位置。offer 失败后走 addWorker() 加新线程并直接提交任务。所以当任务队列满的时候,新来的任务是由临时线程立即执行的,不会排队等待队列空出来。
面试刁钻题 6:线程池什么时候从队列里取任务?
话术:Worker 线程执行完当前任务后,会去 workQueue.poll() 或 take() 取下一个任务。Worker 的 runWorker 方法是死循环,不断取任务执行。取不到就超时后退出(如果是临时线程)或者一直阻塞等(如果是核心线程)。所以队列里的任务不是主动被"推"出去的,而是线程空下来了自己"拉"的。
面试刁钻题 7:线程池的线程做完任务后,是直接销毁还是怎么回到池子里?
话术:做完任务后,Worker 线程进入无限循环:取下一个任务 → 执行 → 取下一个 → 执行。如果取不到任务,分两种情况:核心线程调 workQueue.take() 阻塞等,一直活着;临时线程调 workQueue.poll(keepAliveTime, TimeUnit),超时后还没取到就退出销毁。所以线程做完任务并没有"回到池子"这个概念,它本来就在池子里,只是从忙碌状态变成了等任务状态。
十、线程池的三个核心问题
10.1 什么时候用
| 异步执行耗时操作(发短信、发邮件) | ✅ 要 |
| 多任务并行处理(同时查询多个服务) | ✅ 要 |
| 定时/周期任务 | ✅ 要 |
| Tomcat 接收 HTTP 请求 | ✅ Tomcat 自己已经用了线程池(你不需要自己建) |
| 同步调一个方法 | ❌ 不需要 |
| 简单的 for 循环 | ❌ 不需要 |
10.2 什么时候别用
// ❌ 滥用:方法内部创建线程池,调用一次就创建一个
public void doSomething() {
ExecutorService pool = Executors.newFixedThreadPool(5); // 每次调都 new!
pool.submit(() -> { … });
pool.shutdown();
}
// 这个和每次 new Thread() 没什么区别
// ✅ 正确:线程池是全局的
public class OrderService {
private static final ExecutorService POOL = createPool();
public void doSomething() {
POOL.submit(() -> { … });
}
}
10.3 什么时候注意 ThreadLocal
// 线程池里的线程是复用的
// 如果你用了 ThreadLocal,用完不清除
// 下一个任务用同一个线程时,还能拿到上一个任务的数据!
public void handleRequest() {
try {
// 设置用户信息
UserContext.set(userId);
executor.submit(() -> {
// 这里拿到的可能是别人的 userId!
System.out.println(UserContext.get());
});
} finally {
// ⚠️ 必须清除!
UserContext.clear();
}
}
解决方案:用阿里开源的 transmittable-thread-local 或者在回调里显式传参。
十一、一句话速记
七个参数背下来:核心最大存活时,队工艺拒
核心线程、最大线程、存活时间、时间单位、队列、线程工厂、拒绝策略
执行流程四步走:
核心跑 → 队排队 → 扩到最大 → 拒了
参数设多少?
CPU密集 = CPU核数+1
IO密集 = CPU核数×2 起步
别用Executors,手写TPE
队列用有界,工厂要命名
拒绝CallerRuns,异常别丢掉






