如何处理 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 即可

相关文章
|
21天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
100 38
|
19天前
|
Java
线程池内部机制:线程的保活与回收策略
【10月更文挑战第24天】 线程池是现代并发编程中管理线程资源的一种高效机制。它不仅能够复用线程,减少创建和销毁线程的开销,还能有效控制并发线程的数量,提高系统资源的利用率。本文将深入探讨线程池中线程的保活和回收机制,帮助你更好地理解和使用线程池。
44 2
|
21天前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
58 4
|
21天前
|
Prometheus 监控 Cloud Native
在 Java 中,如何使用线程池监控以及动态调整线程池?
【10月更文挑战第22天】线程池的监控和动态调整是一项重要的任务,需要我们结合具体的应用场景和需求,选择合适的方法和策略,以确保线程池始终处于最优状态,提高系统的性能和稳定性。
89 2
|
28天前
|
监控 Java
在实际应用中选择线程异常捕获方法的考量
【10月更文挑战第15天】选择最适合的线程异常捕获方法需要综合考虑多种因素。没有一种方法是绝对最优的,需要根据具体情况进行权衡和选择。在实际应用中,还需要不断地实践和总结经验,以提高异常处理的效果和程序的稳定性。
19 3
|
28天前
|
监控 Java
捕获线程执行异常的多种方法
【10月更文挑战第15天】捕获线程执行异常的方法多种多样,每种方法都有其特点和适用场景。在实际开发中,需要根据具体情况选择合适的方法或结合多种方法来实现全面有效的线程异常捕获。这有助于提高程序的健壮性和稳定性,减少因线程异常带来的潜在风险。
19 1
|
28天前
|
监控 API
Hook 线程与捕获线程执行异常
【10月更文挑战第11天】Hook 线程和捕获线程执行异常是多线程编程中不可或缺的技术。通过深入理解和掌握这些方法,我们可以提高程序的稳定性和可靠性,更好地应对各种异常情况。同时,在实际应用中要注意平衡性能和准确性,制定合理的异常处理策略,以确保程序的正常运行。
28 1
|
23天前
|
监控 数据可视化 Java
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
如何使用JDK自带的监控工具JConsole来监控线程池的内存使用情况?
|
2月前
|
Java
安装JDK18没有JRE环境的解决办法
安装JDK18没有JRE环境的解决办法
321 3
|
3月前
|
Java 关系型数据库 MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【8月更文挑战第19天】在Linux上搭建Java Web应用环境,需安装JDK 1.8、Tomcat及MariaDB。本指南详述了使用apt-get安装OpenJDK 1.8的方法,并验证其版本。接着下载与解压Tomcat至`/usr/local/`目录,并启动服务。最后,通过apt-get安装MariaDB,设置基本安全配置。完成这些步骤后,即可验证各组件的状态,为部署Java Web应用打下基础。
57 1