线程池内运行的线程抛异常,线程池会怎么办

通过文章《线程池运行的线程和队列中等待的线程是同一个吗》我们了解到线程池中实际运行的是线程池自身的线程,只是在runWorker方法中调用了我们传递进入Runnable对象的run()方法,那么如果run()方法中出现异常了,那么要怎么处理?会不会将我们的线程池停掉?

我们先来看下runWorker()方法的具体逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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();
} 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);
}
}

在代码中我们看到,在调用用户Runnable实例方法run()的时候,进行了try…catch…finally,但是在catch()中是直接将异常抛出了,也就是说并未在while循环内消化掉,而是抛出给外层,这时会将while循环终止掉,然后在外层的try…finally中并未捕获内部传出的异常,所以异常信息会继续往上抛出,我们来关注一下这两层try的finally代码块,内部的finally中执行了一个空的方法afterExecute(),这个方法是留给我们自定义线程池时使用的,和beforeExecute()方法一样,既然是空方法,那我们就先不用去看它了,来看下外层的finally代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 从runWorker方法中传过来的是true,所以这句目前版本中必定会被执行到
// 作用是将当前线程池中的有效线程数-1,意思也就是出现异常的线程会被从线程池中拿掉
// 为什么说是出现异常的线程会被拿掉呢?因为在try内部是一个while循环,除非关闭核心线程或运行中线程出现异常,否则不会执行到这里
if (completedAbruptly)
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 更新完成的任务数,只要是被线程池线程执行过的,不管是否出现异常,都被认为是执行成功的任务
completedTaskCount += w.completedTasks;
// 将当前Worker线程从线程池中移除销毁
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

// 一系列判断,主要是判断是否符合给线程池创建新的线程
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return;
}
// 给线程池创建新的线程,core之所以传递false,是因为这里要防止创建失败
addWorker(null, false);
}
}

通过源码我们看到在处理任务的过程中,如果线程出现异常,则会将该线程从线程池中移除销毁,然后再新创建一个线程加入到线程池中,也就是说在任务发生异常的时候,会终结掉运行它的线程。

我们从源码中得到的信息,现在来验证一下我们的分析

验证代码:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30));
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread t = new Thread(() -> {
System.out.println(10 / finalI - 5);
});
pool.execute(t);
}
}

输出结果:

image-20200527152919795

抛异常了,但是并未影响线程池中的其他任务,我们打断点在processWorkerExit()方法中,看下workers变量的数据

  • 异常发生之前

    image-20200527154429404

  • 异常发生之后

    image-20200527154504520

看到在异常前后,线程1f36e637被移除了,转而创建了一个7073cb62放到了线程池中,而未发生异常的线程578486a3依然存在于线程池中。


小结

通过示例我们验证了一点:当任务出现未被捕获到的异常时,会将执行该任务的线程池中的线程从线程池移除并结束掉,然后移除之后创建一个新的线程放回到线程池中。


上面我们知道了当线程执行的任务发生未被捕获的异常时,会将异常一直往上抛出,那么我们能否在主线程中捕获它进行处理呢?我们来试下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30));
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread t = new Thread(() -> {
System.out.println(10 / finalI - 5);

});
try {
pool.execute(t);
} catch (Exception e) {
System.out.println("发生了异常");
}
}
}

运行结果:

image-20200527165519282

由运行结果可以看出我们并未捕获到线程池中线程抛出的异常,也就是异常并未被抛出到主线程中,这就尴尬了,毕竟这些异常是和业务相关联的,我们却无法捕获和处理,这咋整呢?

忽的一下,想到了线程池的比较重要的一个参数:ThreadFactory接口,这个接口的作用是按需创建新线程的,使用线程工厂消除了对Thread#Thread(Runnable) new Thread的强依赖,使应用程序能够使用特殊的Thread子类、优先级等。大白话就是让线程池中的线程使用我们自定义的线程,这个自定义可不是我们通过execute()或submit()传进来的自定义线程,而是Worker类中的thread变量,也就是实际运行的线程,我们看一下Worker类的构造方法

1
2
3
4
5
6
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 调用ThreadFactory的newThread方法创建线程
this.thread = getThreadFactory().newThread(this);
}

在构造方法中调用线程工厂的newThread()方法创建运行线程,我们上面通过ThreadPoolExecutor的构造方法创建线程池时并未传入ThreadFactory参数,那么就会使用默认的Executors.defaultThreadFactory()来创建线程,它的实现逻辑如下:

1
2
3
4
5
6
7
8
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}

那么我们要是想自定义Worker#thread的值的话,就自定义一个ThreadFactory实现类即可,比如我们可以把线程池创建语句升级为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 5, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(30), r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((t1, e) -> System.out.println("发生了异常"));
return t;
});
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread t = new Thread(() -> {
System.out.println(10 / finalI - 5);

});
pool.execute(t);
}
}

我们使用lambda表达式来创建,使用Thread#setUncaughtExceptionHandler()方法来获取线程内未被捕获的异常,我们运行一下看看结果:

image-20200527175844897

成功捕获了线程内部出现的异常。


那么现在就又有一个问题了:如果我们主动捕获并处理线程内抛出的异常,那么这个线程还会从线程池中移除销毁吗?

我们来试下,还是使用上面的那段代码,然后断点打在processWorkerExit()方法中,看下执行结果

  • 异常之前

    image-20200527180523846

  • 异常之后

    image-20200527180634454

从执行结果来看,发生异常的线程是35d176f7,在异常发生之后同样从线程池中被移除了。


总结

当线程池中线程执行任务的时候,任务出现未被捕获的异常的情况下,线程池会将允许该任务的线程从池中移除并销毁,且同时会创建一个新的线程加入到线程池中;可以通过ThreadFactory自定义线程并捕获线程内抛出的异常,也就是说甭管我们是否去捕获和处理线程池中工作线程抛出的异常,这个线程都会从线程池中被移除。