【JUC进阶】如何优雅地处理线程池异常?

1. 线程池异常的出现

在开发中,我们经常使用线程池,会将不同的任务提交到线程池中,但是如果任务出现了异常,会发生什么呢?该怎么处理呢?怎么获取到异常信息来解决异常?

想要知道如何解决,就需要了解了解线程池提交任务的两个方法executesubmit

void execute(Runnable command);
Future<?> submit(Runnable task);

由源码可见,两个任务最本质的区别就是execute无返回值,而submit由返回值。

接着用两个方法提交一个会抛出异常的任务看看发生什么?

public class Test {
    public static void main(String[] args) throws InterruptedException {

        //创建一个线程池
        ExecutorService executorService= Executors.newFixedThreadPool(1);

        //使用submit提交任务
        executorService.submit(new task());

        //使用execute提交任务
        executorService.execute(new task());

    }


}
// 会抛出异常的任务
class task implements Runnable {
    @Override
    public void run() {
        System.out.println("进入了task方法!!!");
        int i = 1 / 0;
    }
}

image-20230319142626880

由输出结果可见

  1. 当线程池抛出异常后 submit无提示,其他线程继续执行
  2. 当线程池抛出异常后 execute抛出异常,其他线程继续执行新任务

submit提交任务时,即使任务出现异常也不会打印异常信息,这是不友好的,这样开发者就不知道程序是否有异常。

其实,submit方法是将异常信息封装到其类型为Future<?>的返回值中去了,想要获取异常信息,就必须使用get()方法

public static void main(String[] args) throws InterruptedException, ExecutionException {

    //创建一个线程池
    ExecutorService executorService= Executors.newFixedThreadPool(1);

    //使用submit提交任务
    Future<?> submit = executorService.submit(new task());
    submit.get();

    //使用execute提交任务
    executorService.execute(new task());

}

image-20230319143438770


2. 如何获取和处理异常


2.1 使用try-catch

在任务中可能出现异常的地方使用try-catch捕获异常,然后抛出。

class task implements Runnable {
    @Override
    public void run() {
        System.out.println("进入了task方法!!!");
        try {
            int i = 1 / 0;
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

image-20230319143851527


2.2 使用Thread.setDefaultUncaughtExceptionHandler方法捕获异常

java.lang.Thread.setDefaultUncaughtExceptionHandler()方法设置处理程序时调用线程突然终止默认由于未捕获到异常,并没有其他的处理程序被定义为该线程。

public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        sm.checkPermission(
            new RuntimePermission("setDefaultUncaughtExceptionHandler")
                );
    }

     defaultUncaughtExceptionHandler = eh;
 }

内部的uncaughtException是一个处理线程内发生的异常的方法,参数为线程对象t和异常对象e。

image-20230319144448284

因此,可以自己实现一个线程工厂,为每一个线程创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑。

public class Test {
    public static void main(String[] args) throws InterruptedException, ExecutionException {

        //1.实现一个自己的线程池工厂
        ThreadFactory factory = (Runnable r) -> {
            //创建一个线程
            Thread t = new Thread(r);
            //给创建的线程设置UncaughtExceptionHandler对象 里面实现异常的默认逻辑
            t.setDefaultUncaughtExceptionHandler((Thread thread1, Throwable e) -> {
                //出现异常
                if (e != null){
                    e.printStackTrace();
                }
            });
            return t;
        };

        //2.创建一个自己定义的线程池,使用自己定义的线程工厂
        ExecutorService executorService = new ThreadPoolExecutor(
                1,
                1,
                0,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(10),
                factory);

        // submit
        Future<?> submit = executorService.submit(new task());
        submit.get();

        Thread.sleep(1000);
        System.out.println("==================为检验打印结果,1秒后执行execute方法");

        // execute
        executorService.execute(new task());

    }

}

image-20230319145133900

这里使用submit提交任务时,控制台打印的异常信息,其实是因为submit.get();,如果没有调用get方法,控制台只会打印一条异常信息,也就是execute出现异常时候而打印的异常信息。

那么,就说明了submit的返回值内部存有异常信息,那么为什么使用submit提交的任务出现异常的时候,没有打印异常信息呢?

其实是因为submit方法内部已经捕获了异常, 只是没有打印出来,也因为异常已经被捕获,因此jvm也就不会去调用ThreadUncaughtExceptionHandler去处理异常。

这需要结合submit和execute的源码分析


2.2.1 submit和execute源码分析

submit源码中,可以看见,其实底层也是调用了execute方法,只是比execute封装多了一层RunnableFuture,而这个RunnableFuture就是submit的返回值。

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

而在execute中,当任务数量少于核心线程数的时候,会调用addWorker(command, true)为每个任务创建一个Worker去处理这些线程

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        //任务数量少于核心线程数量
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

这个Worker也是一个线程,执行任务时调用的就是Workerrun方法!run方法内部又调用了runworker方法!:

private boolean addWorker(Runnable firstTask, boolean core) {
		.......
            ......
            Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
            	......
}

public void run() {
    runWorker(this);
}

可见,当使用execute提交任务的时候,会被封装成了一个runable任务,然后进去 再被封装成一个Worker,最后在workerrun方法里面调用runWoker方法,runWoker方法里面执行任务任务。

runWorker中,执行线程任务的是task.run();

如果任务出现异常,用try-catch捕获异常往外面抛,我们在最外层使用try-catch捕获到了 runWoker方法中抛出的异常。因此我们在execute中看到了我们的任务的异常信息。

final void runWorker(Worker w) {
     Thread wt = Thread.currentThread();
     Runnable task = w.firstTask;
     w.firstTask = null;
     w.unlock(); // allow interrupts
     boolean completedAbruptly = true;
     try {
      //这里就是线程可以重用的原因,循环+条件判断,不断从队列中取任务        
      //还有一个问题就是非核心线程的超时删除是怎么解决的
      //主要就是getTask方法()见下文③
         while (task != null || (task = getTask()) != null) {
             w.lock();
             if ((runStateAtLeast(ctl.get(), STOP) ||
                  (Thread.interrupted() &&
                   runStateAtLeast(ctl.get(), STOP))) &&
                 !wt.isInterrupted())
                 wt.interrupt();
             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 {
                  //execute的方式可以重写此方法处理异常
                     afterExecute(task, thrown);
                 }
             } finally {
                 task = null;
                 w.completedTasks++;
                 w.unlock();
             }
         }
         //出现异常时completedAbruptly不会被修改为false
         completedAbruptly = false;
     } finally {
      //如果如果completedAbruptly值为true,则出现异常,则添加新的Worker处理后边的线程
         processWorkerExit(w, completedAbruptly);
     }
 }

submit方法t是将任务封装成了一个futureTask ,然后这个futureTask被封装worker成,在wokerrun方法里面,最终调用的是futureTaskrun方法,而在run方法里面,将异常吞掉了,并没有抛出异常,因此在workerrunWorker方法里面无法捕获到异常。

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 {
                 result = c.call();
                 ran = true;
             } catch (Throwable ex) {
                 result = null;
                 ran = false;
                 //在此方法中设置了异常信息
                 setException(ex);
             }
             if (ran)
                 set(result);
         }
         //省略下文
 。。。。。。

run方法中,如果出现了异常,不会将异常往外抛,则是将异常设置给outcome

protected void setException(Throwable t) {
       if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        //将异常对象赋予outcome,记住这个outcome,
           outcome = t;
           UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
           finishCompletion();
       }

submit方法返回值对象Future中,当调用Future.get()时, 会调用内部的report方法

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    //注意这个方法
    return report(s);
}

report中返回值实际上就是上面的outcome

private V report(int s) throws ExecutionException {
 //设置`outcome`
    Object x = outcome;
    if (s == NORMAL)
     //返回`outcome`
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

因此,在使用submit方法提交任务时候,任务对象Runable会被封装程Future类型。

future 里面的 run方法在处理异常时, try-catch了所有的异常,通过setException(ex);方法设置到了变量outcome里面, 可以通过future.get获取到outcome

submit里面,除了从返回结果里面取到异常之外, 没有其他方法。因此,在不需要返回结果的情况下,最好用execute ,这样就算没有写try-catch,疏漏了异常捕捉,也不至于丢掉异常信息。


2.3 重写afterExecute进行异常处理

excute的方法里面,可以通过重写afterExecute进行异常处理,当然也适用于submit,但是因为submit的方式比较麻烦,submittask.run里面把异常吞了,根本不会跑出来异常,因此也不会有异常进入到afterExecute里面。

runWorker里面,调用task.run之后,会调用线程池的 afterExecute(task, thrown) 方法。

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) {
                w.lock();
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                    //直接就调用了task的run方法 
                        task.run(); //如果是futuretask的run,里面是吞掉了异常,不会有异常抛出,
                       // 因此Throwable thrown = null;  也不会进入到catch里面
                    } 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和异常
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

因此,我们在创建线程池的时候可以重写afterExecute方法

但是对于afterExecute处理submit提交的异常的时候,需要进行额外的处理,也就是判断Throwable是否是FutureTask

public class Test {
    public static void main(String[] args) throws InterruptedException, ExecutionException {


        //1.创建一个自己定义的线程池
        ExecutorService executorService = new ThreadPoolExecutor(
                2,
                3,
                0,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue(10)
        ) {
            //重写afterExecute方法
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                //这个是excute提交的时候
                if (t != null) {
                    System.out.println("afterExecute里面获取到excute提交的异常信息,处理异常" + t.getMessage());
                }
                //如果r的实际类型是FutureTask 那么是submit提交的,所以可以在里面get到异常
                if (r instanceof FutureTask) {
                    try {
                        Future<?> future = (Future<?>) r;
                        //get获取异常
                        future.get();

                    } catch (Exception e) {
                        System.out.println("afterExecute里面获取到submit提交的异常信息,处理异常" + e);
                    }
                }
            }
        };
        //当线程池抛出异常后 execute
        executorService.execute(new task3());

        //当线程池抛出异常后 submit
        executorService.submit(new task3());
    }
}

class task3 implements Runnable {
    @Override
    public void run() {
        System.out.println("进入了task方法!!!");
        int i = 1 / 0;
    }
}

image-20230319161241336


参考:面试官:线程池中线程抛了异常,该如何处理?_Java精选的技术博客_51CTO博客

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起名方面没有灵感

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值