核心知识点
1.1 ThreadPoolExecutor 七大参数
自行结合相关流程描述这7个参数
java
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 工作队列
ThreadFactory threadFactory, // 6. 线程工厂
RejectedExecutionHandler handler // 7. 拒绝策略
)
参数详解表
| 参数 | 说明 | 项目中的值 | 面试重点 |
|---|---|---|---|
| corePoolSize | 核心线程数,常驻线程池 | 10 | 即使空闲也不会被回收 |
| maximumPoolSize | 最大线程数 | 20 | 线程池能创建的最大线程数 |
| keepAliveTime | 空闲线程存活时间 | 60秒 | 非核心线程的空闲时间超过此值会被回收 |
| unit | 时间单位 | TimeUnit.SECONDS | 配合keepAliveTime使用 |
| workQueue | 工作队列 | LinkedBlockingQueue(500) | 存储待执行任务的阻塞队列 |
| threadFactory | 线程工厂 | 自定义ThreadFactory | 用于创建线程,可自定义线程名、优先级等 |
| handler | 拒绝策略 | CallerRunsPolicy | 队列满且线程数达到最大值时的处理策略 |
1.2 为什么核心线程数不会被回收
text
核心设计思想:
1. 核心线程是线程池的"常驻军"
2. 减少线程创建/销毁的开销
3. 保证线程池的基本处理能力
4. 即使空闲也保持一定数量,快速响应新任务
1.2.1. 核心机制:核心线程不会被回收的关键在 getTask() 方法。源码逻辑如下:
java
private Runnable getTask() {
// 记录上一次从队列获取任务是否超时
// 只有使用poll()方法时才可能超时,take()方法不会超时
boolean timedOut = false;
for (;;) { // 无限循环,直到获取到任务或线程被回收
int c = ctl.get(); // 获取ctl值(包含线程池状态和线程数)
int rs = runStateOf(c); // 提取线程池运行状态
/*
* 检查线程池状态,判断是否需要回收线程
* 条件1:rs >= SHUTDOWN 线程池已关闭
* 条件2:rs >= STOP(不处理队列任务) 或 workQueue.isEmpty(队列为空)
* 满足条件说明线程池不需要再处理任务,可以回收线程
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount(); // 减少worker计数
return null; // 返回null表示线程需要被回收
}
int wc = workerCountOf(c); // 获取当前工作线程数
/*
* 🔑 核心线程和非核心线程的关键区别判断
*
* timed = true 表示当前线程"可能"会被回收(使用poll超时机制)
* timed = false 表示当前线程"不会"被回收(使用take永久阻塞)
*
* 条件1:allowCoreThreadTimeOut = true(允许核心线程超时,默认false)
* 条件2:wc > corePoolSize(当前线程数超过核心线程数)
*
* 核心线程:默认情况下timed=false,不会被回收
* 非核心线程:timed=true,空闲超时后会被回收
*/
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
* 🔑 线程回收的核心判断逻辑
*
* 条件分解:
* 1. wc > maximumPoolSize:当前线程数超过最大线程数(需要回收)
* 2. (timed && timedOut):允许超时 且 上一次获取任务超时了(需要回收)
*
* 3. (wc > 1 || workQueue.isEmpty()):保证至少保留1个线程 或 队列为空
* - 如果wc > 1:还有其他线程,当前线程可以回收
* - 如果workQueue.isEmpty():队列为空,可以回收线程
*
* 核心线程回收路径:只有allowCoreThreadTimeOut=true时才可能满足条件2
* 非核心线程回收路径:timed=true,当poll()超时后timedOut=true,满足条件2
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 通过CAS减少worker计数,确保线程安全
if (compareAndDecrementWorkerCount(c))
return null; // 返回null,runWorker()循环结束,线程被回收
continue; // CAS失败,重试
}
try {
/*
* 🔑 核心线程和非核心线程的阻塞方式本质区别
*
* timed = true(非核心线程):
* - 使用poll(keepAliveTime, TimeUnit.NANOSECONDS)
* - 带超时的阻塞,keepAliveTime后返回null
* - 下次循环时timedOut=true,满足回收条件
*
* timed = false(核心线程):
* - 使用workQueue.take()
* - 永久阻塞,直到队列中有任务
* - 不会返回null,所以timedOut永远为false
* - 永远不会满足回收条件,线程得以保留
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : // 非核心线程:可能超时返回null
workQueue.take(); // 核心线程:永久阻塞,不会返回null
if (r != null)
return r; // 获取到任务,返回给runWorker()执行
/*
* 只有poll()超时返回null时才会执行到这里
* take()不会超时,所以不会执行到这里
*
* 设置timedOut=true,下次循环时如果timed仍为true
* 就会满足 (timed && timedOut) 的回收条件
*/
timedOut = true;
} catch (InterruptedException retry) {
// 线程被中断,重置timedOut标志
// 如果是take()被中断,会重新循环继续take()
// 如果是poll()被中断,会重新循环继续poll()
timedOut = false;
}
}
}
1.2.2. 核心逻辑解析(其实代码中有注释,这里再次重复下,看懂了上述代码可跳过)
- 关键变量 timed
java
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// allowCoreThreadTimeOut = false(默认)且 wc <= corePoolSize:timed = false → 核心线程
// allowCoreThreadTimeOut = true 或 wc > corePoolSize:timed = true → 非核心线程或允许超时的核心线程
- 核心线程的阻塞方式
java
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANANOSECONDS) : // 非核心线程
workQueue.take(); // 核心线程:永久阻塞
// 核心线程(timed = false):使用 workQueue.take(),永久阻塞等待任务,不会超时返回 null,因此不会被回收
// 非核心线程(timed = true):使用 workQueue.poll(keepAliveTime),超时返回 null,触发回收
// workQueue.take() 是一个阻塞方法 ,它是 BlockingQueue 接口的核心方法之一,专门用于实现 线程间的阻塞等待 。
// Runnable task = workQueue.take(); // 如果队列中没有任务,当前线程会阻塞在这里
// - 当前线程会 暂停执行 , 不会消耗 CPU
// - 直到有其他线程向队列中放入任务(如 execute() 提交任务)
// - 然后 take() 返回该任务,当前线程继续执行
- 回收判断条件
java
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
// 回收线程
return null;
}
// 核心线程:timed = false,即使 timedOut = true,条件 (timed && timedOut) 仍为 false,不会被回收
// 非核心线程:timed = true 且 timedOut = true 时,满足条件,会被回收
1.2.3. 完整执行流程对比
text
核心线程执行流程:
runWorker()
→ getTask()
→ timed = false (因为 wc <= corePoolSize)
→ workQueue.take() // 永久阻塞,等待任务
→ 获取到任务 → 返回任务 → 执行任务
→ 继续循环,再次调用 getTask()
→ 永远不会返回 null
→ 线程永远不会退出 ✅
非核心线程执行流程:
runWorker()
→ getTask()
→ timed = true (因为 wc > corePoolSize)
→ workQueue.poll(keepAliveTime) // 超时阻塞
→ 如果超时 → 返回 null
→ 线程退出循环
→ 线程被回收 ✅
1.2.4. 设计原因
a. 性能考虑
text
线程创建/销毁的开销:
1. 创建线程:
- 分配栈空间(默认1MB)
- 系统调用(创建内核线程)
- 初始化线程上下文
- 开销:约1-10ms
2. 销毁线程:
- 释放栈空间
- 系统调用(销毁内核线程)
- 清理线程上下文
- 开销:约1-5ms
核心线程保持常驻:
✅ 避免频繁创建/销毁线程
✅ 减少系统调用开销
✅ 提高响应速度
b. 响应速度
text
场景对比:
场景1:核心线程被回收
任务到达 → 创建新线程(1-10ms)→ 执行任务
总延迟:1-10ms + 任务执行时间
场景2:核心线程常驻
任务到达 → 立即执行(0ms)→ 执行任务
总延迟:任务执行时间
性能提升:1-10ms(对于短任务,提升明显)
c. 资源利用
text
核心线程的资源占用:
- 每个线程:约1MB栈空间
- 10个核心线程:约10MB内存
- 现代服务器:内存充足,10MB可忽略
收益:
✅ 快速响应新任务
✅ 减少线程创建开销
✅ 提高系统吞吐量
小结:核心线程不会被回收的原因:
-
源码层面:
- getTask() 中,核心线程使用 workQueue.take() 永久阻塞
- 非核心线程使用 workQueue.poll(keepAliveTime) 超时返回
- 只有返回 null 时线程才会退出
-
设计层面:
- 减少线程创建/销毁开销
- 提高响应速度
- 保证基本处理能力
-
特殊情况:
- 设置 allowCoreThreadTimeOut(true) 后,核心线程也会超时回收
1.3 线程池如何实现线程复用
通过Worker机制实现:
1. Worker继承AQS,实现Runnable
2. Worker持有Thread和firstTask
3. 线程执行完任务后,不会销毁
4. 通过while循环不断从队列取任务执行
5. 只有在队列为空且超时时才会退出循环
1.3.1 底层源码剖析
1. Worker 内部类(核心载体)
Java
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread; // 🔑 真正的工作线程
Runnable firstTask; // 第一个任务(可能为 null)
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // 🔑 传入 this
}
public void run() {
runWorker(this); // 🔑 线程启动后执行这里
}
}
🔍 关键点:线程启动后执行的是 Worker.run() ,而 Worker.run() 调用的是 runWorker(this) ,这是一个 无限循环获取任务并执行 的方法。
2. runWorker:线程复用的核心逻辑
JAVA
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
try {
// 🔑 核心循环:线程复用的本质
while (task != null || (task = getTask()) != null) {
w.lock(); // 加锁,防止中断
try {
beforeExecute(wt, task); // 前置钩子
task.run(); // 🔑 直接调用任务的 run 方法
afterExecute(task, null); // 后置钩子
} finally {
task = null; // 任务执行完置空
w.completedTasks++;
w.unlock();
}
// 🔑 循环继续,继续获取下一个任务
}
} finally {
processWorkerExit(w, completedAbruptly); // 线程退出处理
}
}
🔍 关键点:
- task.run() 是普通方法调用, 不会创建新线程
- 一个 Worker.thread 会 循环执行多个任务的 run() 方法
- 线程不会销毁,直到 getTask() 返回 null
3. 线程复用 vs 线程销毁对比
text
传统方式(每次创建新线程):
任务1到达 → 创建线程1 → 执行任务1 → 销毁线程1
任务2到达 → 创建线程2 → 执行任务2 → 销毁线程2
开销:每次任务都需要创建/销毁线程
线程池方式(线程复用):
任务1到达 → 创建线程1 → 执行任务1
任务2到达 → 线程1复用 → 执行任务2
任务3到达 → 线程1复用 → 执行任务3
...
开销:只创建一次线程,后续任务复用
1.3.2. 为什么不是 Thread.start()?
new Thread(task).start() 方式, 会创建新线程,不复用线程
worker.thread + task.run()方式,不会创建新线程,复用线程
✅ 线程池的复用本质: 固定线程 + 循环执行任务的 run() 方法
小结:线程复用的底层原理一句话
线程池创建固定数量的 Worker 线程,这些线程启动后不会销毁,而是通过 while (task = getTask()) 循环从队列中取出任务并执行 task.run() ,从而实现一个线程顺序执行多个任务,达到线程复用的目的。
1.4: 为什么先放队列而不是先创建线程?
1.4.1 源码执行顺序分析
java
public void execute(Runnable command) {
int c = ctl.get();
// 步骤1: 先检查核心线程数
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) // 创建核心线程
return;
c = ctl.get();
}
// 🔑 步骤2: 先尝试放入队列(关键!)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false); // 确保至少有一个线程处理队列
}
// 🔑 步骤3: 队列满了才创建非核心线程
else if (!addWorker(command, false))
reject(command);
}
执行顺序:
- 先检查核心线程数,不足则创建核心线程
- 先尝试放入队列(关键设计)
- 队列满时才创建非核心线程
1.4.2 性能开销对比
创建线程的完整过程:
1. 内存分配:
- 栈空间:默认1MB(可通过-Xss调整)
- 线程本地存储(TLS)
- 线程控制块(TCB)
- 开销:约0.5-2ms
2. 系统调用:
- 创建内核线程(clone系统调用)
- 设置线程属性
- 注册到内核调度器
- 开销:约1-5ms
3. 初始化:
- 初始化线程上下文
- 设置栈指针
- 初始化寄存器
- 开销:约0.5-2ms
总开销:约2-10ms(取决于系统负载)
队列操作(LinkedBlockingQueue.offer()):
1. 内存操作:
- 创建Node节点(约24字节)
- 设置指针引用
- 更新队列计数器
- 开销:纳秒级(<100ns)
2. 同步操作:
- CAS操作(compareAndSwap)
- 内存屏障
- 开销:纳秒级(<100ns)
总开销:约100-500ns(比创建线程快20000倍!)
性能对比表:
| 操作 | 时间开销 | CPU开销 | 内存开销 | 系统调用 |
|---|---|---|---|---|
| 创建线程 | 2-10ms | 高 | 1MB+ | 需要 |
| 队列操作 | 100-500ns | 极低 | 24字节 | 不需要 |
| 性能比 | 20000:1 | - | - | - |
1.4.3 设计优势深度解析
优势1: 充分利用已有线程
场景:核心线程数=10,当前有5个线程在执行任务
任务到达 → 放入队列 → 空闲的5个线程立即从队列取任务执行
结果:充分利用已有线程,无需创建新线程 ✅
优势2: 减少线程创建开销
场景:100个短任务连续到达
先放队列:创建10个核心线程,任务在队列中排队
先创建线程:可能需要创建100个线程(如果允许)
结果:节省90次线程创建,节省约180-900ms ✅
优势3: 平滑处理峰值
场景:突然有1000个任务到达
先放队列:10个线程处理,990个任务在队列中等待
先创建线程:可能创建1000个线程(如果允许),系统压力大
结果:平滑处理,避免系统过载 ✅
优势4: 资源可控
场景:队列容量500,最大线程数20
先放队列:最多20个线程 + 500个任务在队列
先创建线程:可能创建大量线程,资源不可控
结果:资源使用可控,避免OOM ✅
1.4.4 如果反过来会怎样?
假设先创建线程,后放队列:
java
// ❌ 假设的执行流程(错误设计)
public void execute(Runnable command) {
// 步骤1: 先创建线程
if (workerCountOf(c) < maximumPoolSize) {
addWorker(command, false); // 创建线程
return;
}
// 步骤2: 线程满了才放队列
if (workQueue.offer(command)) {
// 放入队列
} else {
reject(command);
}
}
问题:
- ❌ 资源浪费:短时任务高峰会创建大量线程,任务结束后线程空闲
- ❌ 响应延迟:创建线程需要时间,无法立即执行
- ❌ 系统压力:频繁创建/销毁线程增加系统负载
- ❌ 无法复用:已有线程可能空闲,却创建新线程
小结
线程池采用先放队列、后创建线程的策略,主要基于性能、资源利用和系统稳定性。性能方面,队列操作(如 LinkedBlockingQueue.offer())是内存操作,耗时约 100-500 纳秒;创建线程需要分配栈空间(默认约 1MB)、系统调用(如 clone)、初始化线程上下文,总耗时约 2-10 毫秒,性能差距约 20000 倍。资源利用上,先放队列能充分利用已有线程:核心线程执行完任务后会从队列取新任务,避免频繁创建/销毁;队列作为缓冲,可平滑处理任务峰值,避免短时突发导致大量线程创建后又快速空闲。从源码看,execute() 先检查核心线程数,不足则创建核心线程;然后尝试放入队列;队列满时才创建非核心线程。这种设计遵循生产者-消费者模式,解耦生产与消费速度,在保证响应性的同时,减少线程创建/销毁开销,提高系统稳定性和资源利用率。如果反过来先创建线程,会导致资源浪费、响应延迟、系统压力增大和无法复用已有线程等问题。因此,先放队列是线程池的核心设计决策,在性能、资源利用和系统稳定性之间取得平衡。
1.5: 如何合理设置线程池参数?
调优步骤:
1. 监控关键指标
- 线程池大小
- 队列长度
- 任务执行时间
- 任务失败率
2. 分析瓶颈
- 如果队列经常满 → 增加线程数或队列容量
- 如果线程经常空闲 → 减少线程数
- 如果任务执行时间长 → 优化任务逻辑
3. 调整参数
- 根据监控数据逐步调整
- 每次只调整一个参数
- 观察效果后再继续调整
4. 验证效果
- 对比调整前后的性能指标
- 确保没有引入新问题
调优策略决策表:
| 队列使用率 | 线程利用率 | CPU使用率 | 调整动作 |
|---|---|---|---|
| > 80% | 任意 | < 70% | 增加核心线程 |
| < 30% | < 30% | 任意 | 减少核心线程 |
| < 50% | 任意 | > 80% | 减少核心线程 |
| > 50% | 任意 | < 30% | 增加核心线程 |
| > 70% | = 100% | < 70% | 增加最大线程 |
1.6: 为什么不推荐使用Executors?
java
// ❌ FixedThreadPool:使用无界队列
ExecutorService executor = Executors.newFixedThreadPool(10);
// 问题:LinkedBlockingQueue无界,可能导致OOM
// ❌ CachedThreadPool:最大线程数无限
ExecutorService executor = Executors.newCachedThreadPool();
// 问题:maximumPoolSize = Integer.MAX_VALUE,可能创建大量线程
// ❌ SingleThreadExecutor:无界队列
ExecutorService executor = Executors.newSingleThreadExecutor();
// 问题:LinkedBlockingQueue无界,可能导致OOM
// 推荐:手动创建ThreadPoolExecutor,明确参数含义
1.7: 线程池的拒绝策略如何选择?
java
// 1. AbortPolicy(默认):抛出异常
new ThreadPoolExecutor.AbortPolicy()
// 抛出 RejectedExecutionException
// 适用:需要明确知道任务被拒绝
// 2. CallerRunsPolicy:调用者执行(项目使用)
new ThreadPoolExecutor.CallerRunsPolicy()
// 由提交任务的线程执行任务
// 适用:需要背压机制,防止系统过载 ✅
// 3. DiscardPolicy:直接丢弃
new ThreadPoolExecutor.DiscardPolicy()
// 静默丢弃,不抛异常
// 适用:允许丢失任务,追求性能
// 4. DiscardOldestPolicy:丢弃最老任务
new ThreadPoolExecutor.DiscardOldestPolicy()
// 丢弃队列中最老的任务,然后重试提交
// 适用:新任务优先级更高
1.8: 如何优雅关闭线程池?
1. 调用shutdown(),不再接受新任务
2. 调用awaitTermination(),等待已提交任务完成
3. 如果超时,调用shutdownNow()强制关闭
4. 再次awaitTermination(),确保关闭完成
优雅关闭线程池 = "先软后硬" 两步走:
- 先 shutdown() 让线程池 体面下班 ;
- 再 awaitTermination() 限时等待,超时后 shutdownNow() 强制清场 。
1.8.1 模板代码(生产级)
java
public static void gracefulShutdown(ThreadPoolExecutor pool) {
// 1. 先软关闭:不再接收新任务,但已入队的会继续执行
pool.shutdown();
try {
// 2. 设置最大等待时间(根据业务调整)
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 超时后硬关闭:尝试中断正在执行的任务
List<Runnable> dropList = pool.shutdownNow();
log.warn("线程池强制关闭,丢弃任务数:{}", dropList.size());
// 4. 再等待一次,确保真正结束
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
log.error("线程池仍未完全终止,可能存在阻塞任务");
}
}
} catch (InterruptedException e) {
// 5. 当前线程被中断,立即强制关闭
pool.shutdownNow();
Thread.currentThread().interrupt(); // 保持中断状态
}
}
一行总结
先 shutdown() 给任务体面执行的机会,再 awaitTermination() 限时等待,超时后 shutdownNow() 强制中断并记录丢弃任务,全程捕获中断并恢复标志位------这就是优雅关闭线程池的全部秘诀。
1.9: 线程池的生命周期有哪些状态,状态之间如何切换;
🎯 五种状态详解
- RUNNING(运行状态) 🟢
场景:
- 线程池创建后的初始状态
- 可以通过 execute() 或 submit() 提交任务
- SHUTDOWN(关闭状态) 🟡
场景:
- 需要优雅关闭线程池
- 希望处理完队列中的所有任务
- 应用正常关闭流程
触发方式:
java
executor.shutdown();
状态转换:
- RUNNING → SHUTDOWN:调用 shutdown()
- SHUTDOWN → TIDYING:当队列为空且所有工作线程都终止时
- STOP(停止状态) 🔴
触发方式:
JAVA
List<Runnable> notExecutedTasks = executor.shutdownNow();
场景:
- 需要立即停止线程池
- 不关心队列中未执行的任务
- 强制中断正在执行的任务
状态转换:
- RUNNING → STOP:调用 shutdownNow()
- SHUTDOWN → STOP:在 SHUTDOWN 状态时调用 shutdownNow()
- STOP → TIDYING:所有工作线程都终止时
- TIDYING(整理状态) 🟠
触发条件:
- 从 SHUTDOWN 状态:队列为空 && 所有工作线程终止
- 从 STOP 状态:所有工作线程终止
状态转换:
- SHUTDOWN/STOP → TIDYING:满足终止条件时自动转换
- TIDYING → TERMINATED:执行完 terminated() 方法后
说明:
- 这是一个自动过渡状态,无法手动触发
- 在此状态会调用 terminated() 钩子方法
- TERMINATED(终止状态) ⚫
到达条件:
- terminated() 方法执行完成
场景:
- 线程池生命周期结束
- awaitTermination() 方法会在此状态返回 true
🔀 完整的状态转换路径
路径1:优雅关闭
text
RUNNING
↓ shutdown()
SHUTDOWN (处理完队列任务)
↓ 队列空 && 工作线程=0
TIDYING (执行 terminated())
↓ terminated() 完成
TERMINATED
路径2:强制关闭
text
RUNNING
↓ shutdownNow()
STOP (中断所有任务)
↓ 工作线程=0
TIDYING (执行 terminated())
↓ terminated() 完成
TERMINATED
路径3:先优雅后强制
text
RUNNING
↓ shutdown()
SHUTDOWN (等待超时)
↓ shutdownNow()
STOP (中断任务)
↓ 工作线程=0
TIDYING (执行 terminated())
↓ terminated() 完成
TERMINATED
⚠️ 注意事项
- SHUTDOWN vs STOP
- shutdown():温和,等待任务完成
- shutdownNow():粗暴,立即中断
- 中断响应
- 任务需要正确处理 InterruptedException
- 使用 Thread.interrupted() 检查中断状态
- 资源清理
- 重写 terminated() 方法进行清理
- 确保外部资源正确关闭
- 避免状态误判
- isShutdown() 在 SHUTDOWN、STOP、TIDYING、TERMINATED 状态都返回 true
- isTerminated() 只在 TERMINATED 状态返回 true
1.10: 线程池的任务提交有两种,execute() 、submit(), 有啥区别?
JAVA
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
// 直接走 execute()
...
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task); // ① 包装 FutureTask
execute(ftask); // ② 仍然调 execute()
return ftask;
}
submit 本质 = newTaskFor() + execute() ;差异在包装层,不在调度层。( submit 的底层仍然是 execute ,只是多穿了一件 FutureTask 外衣。)
异常:execute 被线程池吃,submit 被 Future 囤,get 时才吐。