一个线程池中的线程异常了,那么线程池会怎么处理这个线程?

线程池常见问题

了解JDK Executors线程池吗?
知道JDK提供了哪些默认的实现吗?
看过阿里巴巴java开发手册吗?知道为啥不允许使用默认的实现吗?
你们没有用默认的吧?那来介绍一下你们自定义线程池的几个常用参数呗?
你这个几个参数的值是怎么得来的呀?算出来的?怎么算出来的?
线程池里面的任务是IO密集型的还是计算密集型的呢?
好,现在我们有一个自定义线程池了,来说一下你这个线程池的工作流程呗?
那你这个线程池满了怎么办呀?拒绝?咋拒绝?有哪些拒绝策略呢?
别紧张,随便说两个就行。

回到开始说的阿里巴巴java开发手册不允许使用默认实现,你回答说可能会引起OOM,那我们聊聊JVM吧

不允许使用的原因

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM(等待队列无界)。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

测试流程

测试用例

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

public class ExecutorsTest {

    public static void main(String[] args) throws Exception {
        ThreadPoolTaskExecutor executor = init();
        executor.execute(() -> sayHi("execute"));
        Thread.sleep(1000);
        executor.submit(() -> sayHi("submit"));
    }

    public static void sayHi(String name) {
        String printStr = "thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name;
        System.out.println(printStr);
        throw new RuntimeException(printStr + " error!!!");
    }

    private static ThreadPoolTaskExecutor init() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("thread_");
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(1000);
        executor.setKeepAliveSeconds(30);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();;
        return executor;
    }
}

抛出堆栈异常为啥对了一半?

在这里插入图片描述
从执行结果我们看出

当执行方式是execute时,可以看到堆栈异常的输出。
当执行方式是submit时,堆栈异常没有输出。

怎么拿到submit的异常堆栈?

在这里插入图片描述
在这里插入图片描述

所以,现在知道为什么回答:抛出堆栈异常只对了一半吧。
execute方法执行时,会抛出(打印)堆栈异常。
submit方法执行时,返回结果封装在future中,如果调用future.get()方法则必须进行异常捕获,从而可以抛出(打印)堆栈异常。

你以为这一部分写到这里就完事了?那不行啊,你心里没有一个疑问吗?为啥execute直接抛出异常,submit没有直接抛出异常呢?

源码查看

执行executes方法时

在java.util.concurrent.ThreadPoolExecutor#runWorker中抛出了异常:
在这里插入图片描述
在_java.lang.ThreadGroup#uncaughtException_进行了异常处理:
在这里插入图片描述
该方法是属于UncaughtExceptionHandler类的

 /**
     * 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);
    }

这个uncaughtException是何许人也,看java doc上咋说的:
在这里插入图片描述
这个方法是JVM调用的,我们只需要指定我们想要的处理方式即可。
那我们怎么指定呢:

        //直接new Thread()的时候
        Thread t = new Thread();
        t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            public void uncaughtException(Thread t, Throwable e) {
                //根据业务场景,做你想做的
            }
        });
        //线程池的时候
        ExecutorService threadPool = Executors.newFixedThreadPool(1, thread -> {
            Thread t1 = new Thread(thread);
            t1.setUncaughtExceptionHandler((Thread t2, Throwable e) ->
                    System.out.println("根据业务场景,做你想做的:" + e.getMessage()));
                    return t1;
                }
        );
执行submit方法时

在这里插入图片描述
其本质也是调用了execute方法,所以它还是回到_java.util.concurrent.ThreadPoolExecutor#runWorker_方法:
在这里插入图片描述
在这里插入图片描述
_java.util.concurrent.FutureTask#setException_干啥了啊,瞅一眼:
在这里插入图片描述
我们马上走向最终的真相:
在这里插入图片描述
好了,第一个议题【抛出堆栈异常为啥对了一半?】讨论完毕。在源码里面走了一趟,现在我们可以给出这一部分的满分答案了。

不影响其他线程任务

这一部分我们直接上代码,运行起来看结果吧:
在这里插入图片描述
代码和运行结果是不会骗人的:
线程池中一个线程异常了后,不影响其他线程任务
大家注意线程名称这个细节:1,2,3,4,6。魔鬼都在细节里啊,这个点我下面会讲,先在这里把问题抛出来:我就纳闷了,怎么没有5啊?!

这个线程会被放回线程池为啥错了

在这里插入图片描述
在这里插入图片描述
5号线程去哪里了?在这里插入图片描述
new Worker()方法会告诉你:5去哪里了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如果使用的是jdk自带的线程池,也是一样的,new Worker最后会执行DefaultThreadFactory里的newThread方法 ,如下:
在这里插入图片描述

再配上这张由我这个灵魂画师亲自操刀画的图,一起食用,味道更佳:

图中线程3发生异常时,线程5已经生成但是任务五还没有被执行,生成新的线程6之后,任务五最后由线程6来执行,所以出现了上面的日志信息(这是个有概率发生的事件)
在这里插入图片描述
现在知道为啥:我回答这个线程会被放回线程池为啥全错了吧。还附带送你一个线程名称变化的细节。

结论

当一个线程池里面的线程异常后:

1、当执行方式是execute时,可以看到堆栈异常的输出

原因:ThreadPoolExecutor.runWorker()方法中,task.run(),即执行我们的方法,如果异常的话会throw x;所以可以看到异常。

2、当执行方式是submit时,堆栈异常没有输出。但是调用Future.get()方法时,可以捕获到异常

原因:ThreadPoolExecutor.runWorker()方法中,task.run(),其实还会继续执行FutureTask.run()方法,再在此方法中c.call()调用我们的方法,
如果报错是setException(),并没有抛出异常。当我们去get()时,会将异常抛出。

3、不会影响线程池里面其他线程的正常执行

4、线程池会把这个线程移除掉,并创建一个新的线程放到线程池中

当线程异常,会调用ThreadPoolExecutor.runWorker()方法最后面的finally中的processWorkerExit(),会将此线程remove,并重新addworker()一个线程。

源码执行流程

execute源码执行流程

1、开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增coreThread去执行任务)。执行到task.run()时会去执行提交的任务。
如果任务执行失败,会throw x抛出异常。
2、之后会到finally中的afterExecute()扩展方法,我们可以扩展该方法对异常做些什么。
3、之后因为线程执行异常会跳出runWorker的外层循环,进入到processWorkerExit()方法,此方法会将执行任务失败的线程删除,并新增一个线程。
4、之后会到ThreadGroup#uncaughtException方法,进行异常处理。
如果没有通过setUncaughtExceptionHandler()方法设置默认的UncaughtExceptionHandler,就会在uncaughtException()方法中打印出异常信息。

submit源码执行流程

1、将传进来的任务封装成FutureTask,同样走execute的方法调用,然后直接返回FutureTask。
2、开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增coreThread去执行任务)。
3、执行到task.run()时,因为是FutureTask,所以会去调用FutureTask.run()。
4、在FutureTask.run()中,c.call()执行提交的任务。如果抛出异常,并不会throw x,而是setException()保存异常。
5、当我们阻塞获取submit()方法结果时get(),才会将异常信息抛出。当然因为runWorker()没有抛出异常,所以并不会删除线程

上述转载内容我替换了几张图片并补充了点内容

本文转载自:
原文作者: 反光的小鱼儿
原文链接:https://www.cnblogs.com/fanguangdexiaoyuer/p/12332082.html#_label2_2

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值