文章目录
-
-
- [1. 情况一:使用 `execute()` 提交任务 ------ 线程会挂掉,并创建一个新线程替代它](#1. 情况一:使用
execute()提交任务 —— 线程会挂掉,并创建一个新线程替代它) -
- [🛠️ 底层源码是怎么跑的?](#🛠️ 底层源码是怎么跑的?)
- [2. 情况二:使用 `submit()` 提交任务 ------ 线程不会死,但异常会被"吞掉"](#2. 情况二:使用
submit()提交任务 —— 线程不会死,但异常会被“吞掉”) -
- [🛠️ 为什么 `submit()` 能保住线程的命?](#🛠️ 为什么
submit()能保住线程的命?)
- [🛠️ 为什么 `submit()` 能保住线程的命?](#🛠️ 为什么
- [3. 面试官追问:线程"死"和"不死"有什么影响?哪种更好?](#3. 面试官追问:线程“死”和“不死”有什么影响?哪种更好?)
- [📝 总结你在面试时的完美答题范式](#📝 总结你在面试时的完美答题范式)
- [1. 情况一:使用 `execute()` 提交任务 ------ 线程会挂掉,并创建一个新线程替代它](#1. 情况一:使用
-
这是一个非常经典的底层源码级面试题。你在深圳晞星智能科技一面时也正好被问到了这个问题 。当时面试官追问你:"运行这个任务的线程会死掉吗?"
答案是:这取决于你是通过 execute() 还是 submit() 来提交任务的。 这两种提交方式会导致完全不同的底层处理机制,结果一个会"死",一个会"活"。
1. 情况一:使用 execute() 提交任务 ------ 线程会挂掉,并创建一个新线程替代它
如果你使用 execute(Runnable command) 提交任务,并且任务在运行期间抛出了未捕获的异常(RuntimeException):
- 线程会不会死? 会死。 该工作线程(Worker)会因为异常而直接终止、退出生命周期。
- 线程池会崩溃吗? 不会。 线程池在底层捕获到了这个异常退出,它会默默地把这个死掉的线程从线程池里移除,然后重新创建一个全新的工作线程(Worker)补上空位,维持核心线程数的平衡。
日志现象: 异常堆栈信息会直接打印到你的控制台或标准错误日志(Stderr)中 ,你不需要主动去写 try...catch 也能看到报错 。
🛠️ 底层源码是怎么跑的?
在 ThreadPoolExecutor 的 runWorker(Worker w) 方法中,底层代码大体是这样实现的:
java
try {
while (task != null || (task = getTask()) != null) {
beforeExecute(wt, task);
try {
task.run(); // 1. 这里如果抛出 RuntimeException,会直接向上抛出
} catch (Throwable x) {
thrown = x; throw x; // 2. 扔给外层
}
}
} finally {
// 3. 线程一旦异常退出,一定会进到这里
processWorkerExit(w, completedAbruptly);
}
在 processWorkerExit(工人退出处理)方法中,有一行核心代码:
java
// 如果是异常退出的(completedAbruptly = true),底层会直接调用 addWorker(null, false);
// 这意味着:旧线程死了,线程池立马原地新开一个线程作为替代品!
if (completedAbruptly)
addWorker(null, false);
2. 情况二:使用 submit() 提交任务 ------ 线程不会死,但异常会被"吞掉"
如果你是用 submit(Callable<T> task) 提交的任务:
- 线程会不会死? 绝对不会死。 线程会完好无损地活下来,并且回到线程池中等待执行下一个任务。
异常去哪了? 异常被线程池"吞"掉了。 如果你不做特殊处理,控制台和日志文件里是一片风平浪静,什么报错都不会打印 。
- 怎么拿到报错?
submit()方法会返回一个Future对象。只有当你调用future.get()去获取结果时,之前执行时发生的异常才会以ExecutionException的形式重新抛出来。
🛠️ 为什么 submit() 能保住线程的命?
因为 submit() 并没有直接把你的 Runnable/Callable 扔给线程运行,而是偷偷在外面包裹了一层 FutureTask 。
在 FutureTask.run() 的底层源码里,它自己把整个异常给 try...catch 住了:
java
public void run() {
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result = c.call();
set(result); // 成功
}
} catch (Throwable ex) {
// 关键点:发生异常不往外抛!而是把异常对象赋值给内部变量 outcome
setException(ex);
}
}
对于工作线程来说,它只是成功执行完了 FutureTask 的 run() 方法,根本没有感知到内部业务报错,所以线程完全不会死。
3. 面试官追问:线程"死"和"不死"有什么影响?哪种更好?
既然 execute() 会让线程死掉、重新建线程;而 submit() 能保住线程,那是不是 submit() 性能更好?
- 创建线程的开销:
execute()导致工作线程频繁死掉和重建是有性能损耗的。 - ThreadLocal 内存泄露问题: 如果你在线程中使用了
ThreadLocal忘记清理,在submit()模式下,线程不死,这个ThreadLocal就会一直常驻内存,引发严重的内存泄露 ;
而在execute()异常死掉的模式下,线程由于直接退出了,它携带的ThreadLocal变量也会随着线程消亡而被垃圾回收(JVM 会回收 Thread 对象的threadLocals映射表)。这也是一个有趣的硬币两面性。
📝 总结你在面试时的完美答题范式
下次如果再遇到这个问题,你可以这样闭环回答:
"这取决于任务的提交方式。
如果是用
execute()提交,当任务抛出未捕获的 RuntimeException 时,该工作线程会直接终止并消亡 。但线程池本身不会崩,它会在finally块的processWorkerExit方法中把死掉的线程移除,并自动创建一个新线程补上,此时错误日志会自动打印在控制台 。如果是用
submit()提交,底层会将任务封装成FutureTask。其内部的run方法会主动用try...catch吞掉异常 并暂存到outcome变量中。因此运行线程不会死 ,它会安全回到线程池。只有当我们调用future.get()时,异常才会被重新抛出。"
这样回答,从结论、底层源码、再到两种机制的对比,完美通关!