线程任务执行报错后,线程会不会挂掉,Java线程池

文章目录

      • [1. 情况一:使用 `execute()` 提交任务 ------ 线程会挂掉,并创建一个新线程替代它](#1. 情况一:使用 execute() 提交任务 —— 线程会挂掉,并创建一个新线程替代它)
        • [🛠️ 底层源码是怎么跑的?](#🛠️ 底层源码是怎么跑的?)
      • [2. 情况二:使用 `submit()` 提交任务 ------ 线程不会死,但异常会被"吞掉"](#2. 情况二:使用 submit() 提交任务 —— 线程不会死,但异常会被“吞掉”)
        • [🛠️ 为什么 `submit()` 能保住线程的命?](#🛠️ 为什么 submit() 能保住线程的命?)
      • [3. 面试官追问:线程"死"和"不死"有什么影响?哪种更好?](#3. 面试官追问:线程“死”和“不死”有什么影响?哪种更好?)
      • [📝 总结你在面试时的完美答题范式](#📝 总结你在面试时的完美答题范式)

这是一个非常经典的底层源码级面试题。你在深圳晞星智能科技一面时也正好被问到了这个问题 。当时面试官追问你:"运行这个任务的线程会死掉吗?"

答案是:这取决于你是通过 execute() 还是 submit() 来提交任务的。 这两种提交方式会导致完全不同的底层处理机制,结果一个会"死",一个会"活"。


1. 情况一:使用 execute() 提交任务 ------ 线程会挂掉,并创建一个新线程替代它

如果你使用 execute(Runnable command) 提交任务,并且任务在运行期间抛出了未捕获的异常(RuntimeException)

  • 线程会不会死? 会死。 该工作线程(Worker)会因为异常而直接终止、退出生命周期。
  • 线程池会崩溃吗? 不会。 线程池在底层捕获到了这个异常退出,它会默默地把这个死掉的线程从线程池里移除,然后重新创建一个全新的工作线程(Worker)补上空位,维持核心线程数的平衡。

日志现象: 异常堆栈信息会直接打印到你的控制台或标准错误日志(Stderr)中 ,你不需要主动去写 try...catch 也能看到报错 。

🛠️ 底层源码是怎么跑的?

ThreadPoolExecutorrunWorker(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); 
    }
}

对于工作线程来说,它只是成功执行完了 FutureTaskrun() 方法,根本没有感知到内部业务报错,所以线程完全不会死。


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() 时,异常才会被重新抛出。"

这样回答,从结论、底层源码、再到两种机制的对比,完美通关!

相关推荐
Hwang25217 小时前
Spring 框架- 容器单例池的理解
java
yh弓长17 小时前
算法积累笔记
java·算法
LeocenaY17 小时前
C/C++ 面试题总结
java·c++·面试
雨落在了我的手上17 小时前
初识java(十一):继承
java·开发语言
XS03010617 小时前
MyBatis关联映射
java·mybatis
码农小旋风17 小时前
IDEA 不只接 Claude 和 Codex:本地模型和第三方 API 也能直接用
java·ide·人工智能·chatgpt·intellij-idea·claude
骆驼整理说17 小时前
Cursor辅助编程工具
java·ai编程
xiep143833351017 小时前
华为系列服务器开启Monitor/MWAIT
java·服务器·网络
yaoxin52112317 小时前
417. 现代 Java IO 最佳实践 - 高效遍历、ZIP 处理与临时文件管理
java·开发语言·windows