Bootstrap

线程池线程执行异常后会怎么样

一、execute和submit

场景一

public class Test17 {

    static ExecutorService executorService = new ThreadPoolExecutor(200, 400, 30, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000), new ThreadFactoryBuilder().setNameFormat("thread-%d").build());
    
    public static void main(String[] args) {
        executorService.execute(() -> doExecute("execute方法"));
        executorService.submit(() -> doExecute("submit方法"));
    }

    private static void doExecute(String methodName) {
        String str = Thread.currentThread().getName() + "|" + methodName;
        System.out.println(str);
        throw new RuntimeException(str + "【异常】");
    }
}

execute方法可以打印出异常堆栈,submit方法没有打印出异常堆栈

场景二 

public class Test17 {

    static ExecutorService executorService = new ThreadPoolExecutor(200, 400, 30, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000), new ThreadFactoryBuilder().setNameFormat("thread-%d").build());

    public static void main(String[] args) {
        Future<?> future = executorService.submit(() -> doExecute("submit方法"));
        try {
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void doExecute(String methodName) {
        String str = Thread.currentThread().getName() + "|" + methodName;
        System.out.println(str);
        throw new RuntimeException(str + "【异常】");
    }
}

submit方法在执行时没有打印出异常堆栈,在调用future.get方法时打印出异常堆栈 

分析

execute方法

在执行java.util.concurrent.ThreadPoolExecutor#execute方法时最终会调用到java.util.concurrent.ThreadPoolExecutor#runWorker方法

 在执行task.run()方法时是有异常捕获的,捕获到异常后会向上抛出异常

submit方法

在执行java.util.concurrent.ExecutorService#submit(java.lang.Runnable)方法时最终会调用到java.util.concurrent.FutureTask#run方法

 上图中c.call()方法是执行我们的doExecute方法,当我们案例中的doExecute正常执行完后,会执行set方法设置返回值,当catch捕获异常后会调用setException方法,并没有向上抛出异常

那继续看下set方法和setException方法

set方法会将返回值赋值给outcome变量

 setException方法中将异常赋值给outcome变量

到此submit方法主要流程就执行完了,这也就解释的通为什么submit方法在执行发生异常时为什么没有打印异常堆栈

那在看下java.util.concurrent.Future#get()方式是如何处理的

 通过代码发现get方法会调用report方法,而在report方式中会去获取submit方法中赋值的outcome变量值,如果outcome是一个异常对象则report方法会将其封装为ExecutionException并抛出

二、线程池里的线程会怎么样

场景一

public class Test17 {

    static ExecutorService executorService = new ThreadPoolExecutor(4, 4, 30, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000), new ThreadFactoryBuilder().setNameFormat("thread-%d").build());

    public static void main(String[] args) {
        for (int i = 1; i <= 12; i++) {
            int j = i;
            executorService.execute(() -> doExecute("" + j));
            //保证线程顺序执行,睡一会
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void doExecute(String num) {
        String str = Thread.currentThread().getName() + "|" + num;
        try {
            if ("3".equals(num)) {
                throw new RuntimeException(str + "【异常】");
            }
            System.out.println(str);
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }
}

线程池一共创建了4个线程,分别是thread-1到thread-4。当num=3的时候thread-2发生了异常,当thread-2发生异常后依然可以正常执行其他的任务

场景二

public class Test17 {

    static ExecutorService executorService = new ThreadPoolExecutor(4, 4, 30, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(2000), new ThreadFactoryBuilder().setNameFormat("thread-%d").build());

    public static void main(String[] args) {
        for (int i = 1; i <= 12; i++) {
            int j = i;
            executorService.execute(() -> doExecute("" + j));
            //保证线程顺序执行,睡一会
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void doExecute(String num) {
        String str = Thread.currentThread().getName() + "|" + num;
        if ("3".equals(num)) {
            throw new RuntimeException(str + "【异常】");
        }
        System.out.println(str);
    }
}

线程池一共创建了4个线程,当num=3的时候thread-2发生了异常,thread-2发生异常后不在执行其他的任务

 疑问:场景一和场景二的唯一区别是场景一在执行任务时任务方法是有异常捕获的,场景二中,在num=4时创建的线程是thread-4,为什么不是thread-3,那在执行num=4的时候thread-3去哪了?为什么mun=7、num=11时thread-3又出来执行任务了?

分析

首先从java.util.concurrent.ThreadPoolExecutor#execute源码入手,线程启动后会调用java.util.concurrent.ThreadPoolExecutor#runWorker方法

 从runWorker方法可以可出,在执行task.run方法时如果发生异常,异常被捕获后并抛出,此时会跳出while循环,将变量 completedAbruptly赋值为false,执行finally里的processWorkerExit方法,那继续看下processWorkerExit方法方法

从processWorkerExit方法可以看到由于completedAbruptly值为false,所以会执行到 workers.remove方法,将抛出异常的线程remove掉,然后在调用addWorker方法,这个方法名看着像是会新加一个Worker。继续看下addWorker方法

 

 addWorker方法会通过new Worker方法创建一个新的Worker,在Worker构造方法内会新建一个线程,线程编号会进行自增加一

在案例二中,当thread-2执行抛出异常后,线程池会将这个异常线程移除,并创建一个新的线程放入线程池,如果采用默认策略,新创建的线程id会在最大线程id上进行自增加一,因为案例中在thread-2执行时最大的线程id=2,所以此时线程池新创建的线程是thread-3,此时线程池中的线程数量依然是3个,所以在下一个任务执行时会在新建一个线程,就是thread-4

 结论

在使用ThreadPoolExecutor的execute方法时,如果线程抛出了未经捕获的运行时异常这个线程会被移除线程池并新建一个线程放到线程池中

本例中的讲解是基于execute方法展开的,submit方法其实调用的还是execute方法,只是在方法执行时传入的任务类型是FutureTask,最终执行的run方法是FutureTask的run方法,那submit方法与execute方法的区别在于执行任务时会不会抛出异常,感兴趣的可以测试一下

 

;