线程池源码-异常处理

本文深入探讨线程池中的异常处理机制,包括默认处理方式的问题及多种自定义解决方案,如内部捕获、FutureTask捕获、重写afterExecute()方法及实现Thread.UncaughtExceptionHandler接口。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在线程池源码系列文章 线程池源码-线程被全部关闭了吗 中有提到,线程池在结束 worker 线程时会有一个标识 completedAbruptly,用来判断线程是否为异常退出。

那什么时候线程会异常退出呢?答案很明显,在执行任务的过程中抛出了异常,且没有进行 try catch 处理。

本篇文章主要探索下面的问题

  • 线程池默认的异常处理方式,存在的问题
  • 关于异常处理的解决方案

默认处理


想知道线程池对异常的默认处理方式,我们先来看看下面的测试代码:

    public static void main(String[] args){
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                System.out.println("current thread: " + Thread.currentThread().getName());
                int res = 1 / 0;
            });
        }
    }

运行结果,可以看到,控制台打印出了异常的堆栈信息:

线程池源码-任务执行 中有提到任务执行的过程,直接看到相关的核心代码:

    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        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 默认的异常处理方式,在控制台打印堆栈的异常信息。

使用默认的处理方式,显然是不切合实际的,被你 leader 看到这样处理异常,岂不把你打屎???在很多场景下,我们需要捕获这个异常,不管是进行特殊处理,还是记录日志,方便后续排查问题。

解决方案


内部捕获

这是最直接简单的方法,在任务执行的过程中捕获可能发生的异常。但同时你无法预知程序运行过程中可能发生的所有异常,当然你也可以用一个超级无敌大的 try {} catch{} 把所有代码都包住,确保万无一失,这种写法极其不合理且丑陋,代码这边就不赘述了。

FutureTask

JDK 提供了另外一种更优雅的处理方式,线程池提交任务还可以使用 submit() 方法,这个方法可以返回执行结果,包括异常信息。

            threadPool.submit(() -> {
                System.out.println("current thread: " + Thread.currentThread().getName());
                Object object = null;
                // 此处会抛空指针异常
                System.out.print(object.toString());
            });

通过 submit() 提交任务, Runnable 对象被一步封装成了 FutureTask 对象, FutureTask 对异常进行了特殊处理,可以通过 futureTask.get() 来获取任务执行过程中抛出的异常,后续的文章会对FutureTask 进行较为详细地解析,这边暂不详细赘述。

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

修改一下测试代码:

    public static void main(String[] args){
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        List<Future> futures = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Future future =  threadPool.submit(() -> {
                System.out.println("current thread: " + Thread.currentThread().getName());
                int res = 1 / 0;
            });
            futures.add(future);
        }

        // 捕获异常
        futures.forEach(future -> {
            try {
                future.get();
            } catch (Exception e) {
                System.out.println("捕获异常信息:" + e.getMessage());
            }
        });
    }

执行结果如下,可以看到异常堆栈被打印出来:

FutureTask 确实能够捕获子线程执行过程中抛出的异常,**但它需要显式地去遍历 Future 结果集,通过 future.get() 去获取程序异常,**似乎也不是太优雅的做法。

线程池接口扩展

线程池在任务执行完毕之后,会调用 afterExecute() 方法,可以看到其将运行结果,以及捕获的异常都作为参数传入方法中。

                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        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();
                }

而 afterExecute()方法默认是没有实现的,是线程池提供给外部的扩展接口。可以通过重写此方法,对异常进行统一处理。

class ExtendThreadPool extends ThreadPoolExecutor {

    public ExtendThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t != null) {
            System.out.println("捕获异常信息:" + t.getMessage());
        }
    }
}

运行结果如下,异常也被统一处理了:

线程接口扩展

前面的做法是在线程池的纬度去做异常处理,那有没有办法从线程的纬度呢?

JDK 也提供了相关的接口,JVM 监控到某个线程因未捕获的异常退出时,会调用该线程预先设置的异常处理器,如果没有提供任何的异常处理器,那么默认的行为就是将堆栈信息输出到控制台。

要自定义线程池中的线程,可以通过继承 ThreadFactory 工厂类,另需要实现 Thread.UncaughtExceptionHandler 接口,进行定制化的异常处理,并将 handler 对象设置到创建完成的线程上。

class customThreadFactory implements ThreadFactory {
    private ThreadFactory threadFactory = Executors.defaultThreadFactory();
    private Thread.UncaughtExceptionHandler exceptionHandler;

    public customThreadFactory(Thread.UncaughtExceptionHandler exceptionHandler) {
        this.exceptionHandler = exceptionHandler;
    }

    public Thread newThread(Runnable r) {
        Thread t = threadFactory.newThread(r);
        t.setUncaughtExceptionHandler(exceptionHandler);
        return t;
    }
}

class ThreadExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("捕获异常信息:" + e.getMessage());
    }
}

public class MainTest {
    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),
                new customThreadFactory(new ThreadExceptionHandler()));
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                System.out.println("current thread: " + Thread.currentThread().getName());
                int res = 1 / 0;
            });
        }
    }
}

运行结果如下:

总结


今天的内容主要涉及任务执行过程中的异常处理,除了提到线程池的默认处理方式,还分享了几种自定义处理方案:

  • 任务内部对异常进行捕获
  • FutherTask 捕获异常
  • 重写线程池的 afterExecute() 方法
  • 实现 Thread.UncaughtExceptionHandler 接口

希望通过本篇文章,你对子线程的异常处理有了更深刻的认识,如果觉得文章对你有帮助,欢迎留言点赞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值