线程任务执行报错后,线程会不会挂掉,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() 时,异常才会被重新抛出。"

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

相关推荐
郝学胜-神的一滴5 分钟前
完全二叉树与堆底层原理深度剖析 | 手写C++大顶堆实现
java·开发语言·数据结构·c++·python·算法
农民小飞侠8 分钟前
[leetcode] 165. Compare Version Numbers
java·算法·leetcode
砍材农夫19 分钟前
物联网实战|Spring Boot + Netty 搭建 MQTT 消息路由与流转层
java·spring boot·后端·物联网·spring
黄毛火烧雪下22 分钟前
Java 基础笔记:文件、递归与字符编码
java·开发语言·笔记
学计算机的计算基24 分钟前
链表算法上篇:LeetCode 206/234/141/142/160/21 题解与易错点
java·笔记·算法·链表
信也科技布道师26 分钟前
从Istio 503 NC 错误深入理解 Mesh 路由全链路原理
java·服务器·网络
swordbob30 分钟前
3 大 I/O 模型BIO / NIO / AIO
java·linux·spring
Pluto_CSND32 分钟前
Cron表达式使用说明
java
十五喵源码网32 分钟前
基于SpringBoot2+vue2的酒店客房管理系统
java·毕业设计·springboot·论文笔记
疯狂成瘾者1 小时前
Java 常用工具包 java.util
java·开发语言·windows