如何处理 JDK 线程池内线程执行异常

简介: 如何处理 JDK 线程池内线程执行异常

带着问题看文章

1、线程池如何输出打印运行任务时抛出的异常?

2、线程池 execute()、submit() 处理异常是否一致?

3、都有哪些方式可以处理任务异常?

根据上述问题, 通过示例代码以及源码共同解析

如无特别标注, 文章阅读以 JDK 1.8 为准

如何处理运行任务时抛出的异常

这个问题我们以 execute() 为例, 先看下源码中是如何处理的

如果看过前面两篇线程池文章的小伙伴对第一个任务执行流程是比较清晰的

execute() -> addWorker() -> start() -> run() -> runWorker()

在这里直接放 ThreadPoolExecutor#runWorker 源码

final void runWorker(Worker w) {
    ...
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
                        ...
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                      /**
                       * 运行提交任务的run方法
                       * 如果抛出异常会被下面的catch块进行捕获
                       */
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x;
                    throw x;
                } catch (Error x) {
                    thrown = x;
                    throw x;
                } catch (Throwable x) {
                    thrown = x;
                    throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

当提交到线程池的任务执行抛出异常时, 会被下方的 catch 块捕获, 向 JVM 抛出异常

我们看下述测试代码, 逻辑比较简单, 创建一个线程池, 提交第一个任务时抛出运行时异常

不必关心示例代码中线程池构建参数
@Test
public void executorTest() {
    ThreadPoolExecutor pool =
            new ThreadPoolExecutor(1, 3, 1000, TimeUnit.HOURS, new LinkedBlockingQueue(5));
    pool.execute(() -> {
        int i = 1/0;
    });

    pool.shutdown();
}
/**
 * Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
 *     at cn.hsa.tps.ThreadPoolExceptionTest.lambda$executorTest$0(ThreadPoolExceptionTest.java:22)
 *     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
 *     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
 *     at java.lang.Thread.run(Thread.java:748)
 */

抛出异常这个都可以理解, 但是线程池内部只是将异常进行 throw 操作, 异常信息是如何被打印的呢?

带着疑惑搜了万能的 "度娘", 最终得到答案是:

向上抛出的异常会由虚拟机 JVM 进行调用 Thread#dispatchUncaughtException

/**
 * Dispatch an uncaught exception to the handler. This method is
 * intended to be called only by the JVM.
 */
private void dispatchUncaughtException(Throwable e) {
    getUncaughtExceptionHandler().uncaughtException(this, e);
}

我们查看注释也能得到相关的信息

向处理程序分配一个未捕获的异常, dispatchUncaughtException 方法仅能被 JVM 调取

而这个处理未捕获异常的 "程序" 就是 UncaughtExceptionHandler

继续查看相关方法, Thread#getUncaughtExceptionHandler

public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() {
    return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
}

这里会查看线程是否有未捕获处理策略, 没有则使用 默认线程组的策略 执行

private ThreadGroup group;
public class ThreadGroup implements Thread.UncaughtExceptionHandler {...}

而线程组中牵扯到批量管理线程,如批量停止或挂起等概念, 这里不过多分析

获取到具体执行策略后, 我们查看下 ThreadGroup#uncaughtException 是如何处理的

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
                Thread.getDefaultUncaughtExceptionHandler();
        if (ueh != null) {
            ueh.uncaughtException(t, e);
        } else if (!(e instanceof ThreadDeath)) {
              // 最终会调用到这里将一场堆栈信息进行打印
            System.err.print("Exception in thread \""
                    + t.getName() + "\" ");
            e.printStackTrace(System.err);
        }
    }
}

其实也就相当于 将异常吞掉, 但是会打印出异常具体的异常信息,到这里我们也能够明白, 线程池中抛出的异常最终落脚点在哪了

execute()、submit() 处理异常是否一致

还是上面的程序, 只不过这次将 execute() 换成了 submit()

@Test
public void executorTest() {
    ThreadPoolExecutor pool =
            new ThreadPoolExecutor(1, 3, 1000, TimeUnit.HOURS, new LinkedBlockingQueue(5));
    pool.submit(() -> {
        int i = 1/0;
    });

    pool.shutdown();
}

明了的小伙伴相信看到这里也会知道, 这里不会进行异常信息打印

为什么不会打印? 这个问题跟着源码我们一起看

ThreadPoolExecutor#runWorker

可以看出 task 已不再是相关的 Runnable 对象, 而是 FutureTask

继续查看 FutureTask 源代码是如何执行的

public void run() {
    if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                    null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                  // 这里内部也是调用的run方法
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        ...
}

通过源码得知, 执行任务流程抛出的异常会被 catch 住, 所以不会将异常信息默认打印

那如何能够知道 submit() 是否抛出了异常?

submit() 返回值是 Future 接口, 默认实现是 FutureTask, 内部有一个 get() 方法, 既可以获取返回值, 同时也可以抛出对应异常

public V get() throws InterruptedException, ExecutionException {
    int s = state;
      // 如果新创建或者未完成的Future会被阻塞
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
      // 🚩 重点
    return report(s);
}

private V report(int s) throws ExecutionException {
    Object x = outcome;
      // 正常完成返回结果
    if (s == NORMAL)
        return (V) x;
      // 任务被取消抛出异常
    if (s >= CANCELLED)
        throw new CancellationException();
      // 这里就是任务内部执行出错返回
    throw new ExecutionException((Throwable) x);
}

在任务发生异常时, 会将异常进行报错, 通过 get() 方法可以返回任务执行异常

都有哪些方式可以处理任务异常

1、任务内部将可能会抛出异常的步骤进行 try catch

@Test
public void executorTest() {
    ThreadPoolExecutor pool =
            new ThreadPoolExecutor(1, 3, 1000, TimeUnit.HOURS, new LinkedBlockingQueue(5));
    pool.execute(() -> {
        try {
            int i = 1/0;
        } catch (Exception ex) {
            ...
        }
    });

    pool.shutdown();
}

在 catch 块中对异常进行处理, 重试处理或者异常报警等操作

submit 同上

2、重写线程 UncaughtExceptionHandler

一般我们创建线程池时都会使用线程工厂, 在创建线程工厂时可以指定 UncaughtExceptionHandler 处理未捕获异常策略

@Test
public void executorTest() {
    Thread.UncaughtExceptionHandler sceneHandler = new Thread.UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            // 仅举例, 不同场景各不相同
            log.error("  >>> 线程 :: {}, 异常处理... ", t.getName(), e);
        }
    };

    ThreadFactory threadFactory = new ThreadFactoryBuilder()
            .setUncaughtExceptionHandler(sceneHandler)
            .setDaemon(true).setNameFormat("根据线程池作用命名").build();

    ThreadPoolExecutor pool =
            new ThreadPoolExecutor(1, 3, 1000, TimeUnit.HOURS, new LinkedBlockingQueue(5), threadFactory);
    pool.execute(() -> {
        int i = 1 / 0;
    });

    pool.shutdown();
}

示例代码也比较简单, 自定义 UncaughtExceptionHandler 处理策略, 创建线程工厂时指定自定义处理策略, 将线程工厂赋值线程池

3、重写线程池的 afterExecute 方法

@Test
public void executorExceptionAfterExecuteTest() {
    ThreadPoolExecutor pool =
            new ThreadPoolExecutor(1, 3, 1000, TimeUnit.HOURS, new LinkedBlockingQueue(5)) {
                @Override
                public void afterExecute(Runnable r, Throwable t) {
                    // 仅举例, 不同场景各不相同
                    log.error("  >>> 异常处理... ", t);
                }
            };
    ...
}

异常处理总结

以上三种方式由于第二种第三种的粒度以及处理不友好, 所以在日常工作中 直接使用第一种任务内 try catch 即可

相关文章
|
6天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
7天前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
38 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
7天前
|
监控 数据可视化 Java
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
使用JDK自带的监控工具JConsole来监控线程池的内存使用情况
|
17天前
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
32 3
|
23天前
|
数据采集 Java Python
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
python 递归锁、信号量、事件、线程队列、进程池和线程池、回调函数、定时器
|
24天前
|
Java
线程池中线程抛了异常,该如何处理?
【8月更文挑战第27天】在Java多线程编程中,线程池(ThreadPool)是一种常用的并发处理工具,它能够有效地管理线程的生命周期,提高资源利用率,并简化并发编程的复杂性。然而,当线程池中的线程在执行任务时抛出异常,如果不妥善处理,这些异常可能会导致程序出现未预料的行为,甚至崩溃。因此,了解并掌握线程池异常处理机制至关重要。
104 0
|
25天前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
49 1
|
7天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
25 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
9天前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
30 10