重拾线程——异常处理(1)

303 阅读5分钟

关于线程的异常,我们可以先看一个小栗子🌰:

fun main(args: Array<String>) {
    println("-------1---主线程开始执行")
    try{
        Thread{
            println("-------2---子线程开始执行")
            throw Exception("error!!!")
            println("-------3---子线程异常后执行")
        }.start()
    }catch (e: Exception){
        println("-------4--我捕获了异常")
    }

    println("-------4---主线程开启子线程后执行")
    Thread.sleep(1000)
    println("-------5---主线程 sleep 后执行")
}

执行结果:

232b8adf491d4525bccfb1f2e8357930_tplv-k3u1fbpfcp-zoom-1.jpeg

可以初步得出以下结论:

  • 线程发生异常后,后续代码不会执行
  • 一个线程发生异常,不会影响到其它线程的执行
  • 无法简单的通过 try catch 线程来捕获线程的异常

虽然,我们可以通过 try catch 代码块来捕获异常,但是我们不太可能对所有的代码块进行 try catch 操作,而且,在团队协作中,也很难要求所有人都能够按需捕获异常,所有,我们先来看看捕获异常有哪几种方式,然后按需地采取相应地解决方案。

setUncaughtExceptionHandler

在创建异常的时候,设置 UncaughtExceptionHandler,等到该线程发生异常的时候,就会调用该 UncaughtExceptionHandleruncaughtException 方法:

    val thread = Thread {
        throw Exception("我是测试异常")
    }
    thread.setUncaughtExceptionHandler { thread, throwable ->
        println("发生异常的线程名字${thread.name}, 异常:${throwable.message}")
    }
    thread.start()

输出结果:

发生异常的线程名字Thread-0, 异常:我是测试异常

该方式适合于对单个线程进行控制,若要对线程组进行控制的时候,就需要使用到 ThreadGroup

ThreadGroup

使用步骤基本如下:

  • 创建 ThreadGroup 的继承类,并重写 uncaughtException 的方法
  • 在后续创建线程的时候,传入 ThreadGroup 的实例,表示给线程为该线程组
  • 后续线程发生异常,就会回调到的 ThreadGroupuncaughtException 方法中

具体代码如下:

    val threadGroup = object : ThreadGroup("线程组1") {
        override fun uncaughtException(t: Thread, e: Throwable) {
            super.uncaughtException(t, e)
            println("发生异常的线程组:${name} 线程名字${t.name}, 异常:${e.message}")
        }
    }
    val thread = Thread (threadGroup){
        throw Exception("我是测试异常")
    }
    thread.start()

运行结果:

此时可以看出,该线程是已经抛出了异常,然后被线程组进行捕获,然后进行处理,所以,这个跟 setUncaughtExceptionHandler 是不太一样的,setUncaughtExceptionHandler 是直接对线程进行捕获处理了,不会再向上层进行抛出异常,所以,当 setUncaughtExceptionHandlerThreadGroup 配合使用的时候,我们需要注意下这个特点。 在这里,我给示例来方便大家理解,如何正确处理 setUncaughtExceptionHandlerThreadGroup 的交互。

    val threadGroup = object : ThreadGroup("线程组1") {
        override fun uncaughtException(t: Thread, e: Throwable) {
            super.uncaughtException(t, e)
            println("发生异常的线程组:${name} 线程名字${t.name}, 异常:${e.message}")
        }
    }
    val thread = Thread (threadGroup){
        throw Exception("我是测试异常")
    }
    val defaultUncaughtExceptionHandler = thread.uncaughtExceptionHandler
    thread.setUncaughtExceptionHandler { thread, throwable ->
        if (throwable.message!!.contains("测试")){
            println("发生异常的线程名字${thread.name}, 异常:${throwable.message}")
        }else{
            defaultUncaughtExceptionHandler.uncaughtException(thread, throwable)
        }
    }
    thread.start()

重点逻辑:

  • 先保存 Thread 默认的 uncaughtExceptionHandler ,再设置 setUncaughtExceptionHandler
  • 当线程发生异常的时候,依据情况分别处理:
    • 不向上抛异常,就无需额外代码
    • 向上抛出异常,则需要调用defaultUncaughtExceptionHandler.uncaughtException(thread, throwable)

下面我们来看看具体的运行结果:

throw Exception("我是测试异常") 的情况:

throw Exception("我是异常") 的情况:

写到这里,可能有人就有疑问了,使用 ThreadGroup 也太麻烦了吧,每次创建线程都需要传入 ThreadGroup ,有没有一种方式,直接给全部线程直接设置异常捕获。

这当然也是有的,setDefaultUncaughtExceptionHandler 就是一种解决方案。

setDefaultUncaughtExceptionHandler

在最开始的时候,我们通过 setUncaughtExceptionHandler 给单独线程设置异常捕获,而其实,每个线程都有一个默认的 DefaultUncaughtExceptionHandler,我们重新设置默认的DefaultUncaughtExceptionHandler 就可以统一捕获线程中的异常了。

基本操作如下:

    Thread.setDefaultUncaughtExceptionHandler{ thread, throwable ->
        println("发生异常的线程名字${thread.name}, 异常:${throwable.message}")
    }
    
    val thread = Thread {
        throw Exception("我是测试异常")
    }
    thread.start()

输出结果:

这个看起来也很简单,但是有两个要特别注意的点。

第一点:假如你是 Android 开发者的话,你有可能会通过这种方式去捕获 Android 全局异常,然后进行日志写入,然后杀死进程,重新启动 App。

但是,通过我们的测试发现,其它子线程发生异常也会进入这个异常捕获,这时就要判断好是不是主线程发生异常的,总不能因为子线程发生异常就把整个 App 重启了吧,没判断好,那就是妥妥地负优化了。😂

第二点:setDefaultUncaughtExceptionHandlersetUncaughtExceptionHandler 共同使用是有可能产生冲突的,就像 setUncaughtExceptionHandlerThreadGroup 共同使用那样,所以,必要时,我们需要解决这种冲突。

由于处理逻辑跟 setUncaughtExceptionHandlerThreadGroup 兼容逻辑差不多,所以这里仅提供示例,就不赘述了。

    val newUncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
        println("全局捕获:发生异常的线程名字${t.name}, 异常:${e.message}")
    }
    Thread.setDefaultUncaughtExceptionHandler(newUncaughtExceptionHandler)
    val thread = Thread {
        throw Exception("我是测试异常")
    }
    thread.setUncaughtExceptionHandler { thread, throwable ->
        if (throwable.message!!.contains("测试")){
            println("子线程捕获:发生异常的线程名字${thread.name}, 异常:${throwable.message}")
        }else{
            newUncaughtExceptionHandler.uncaughtException(thread, throwable)
        }
    }
    thread.start()

有兴趣的同学可以自行解决下 DefaultUncaughtExceptionHandlernewUncaughtExceptionHandlersetUncaughtExceptionHandlerThreadGroup 共同存在时的异常处理,了解下如何层层把异常上报上去。

以上的异常捕获都是基于 Thread 的,但是,由于 Thread 没有返回值,我们有时会用到 FetureTask,那下面我们就来了解下,有没有其它方式捕获 FetureTask 的异常。

FetureTask 的异常捕获

首先,我们先写下示例代码来让 FetureTask 抛出异常:

    val futureTask = FutureTask {
        println("futureTask 正在执行")
        throw Exception("我是测试异常")
        return@FutureTask "1"
    }
    Thread(futureTask).start()

运行结果:

说好的异常怎么不见了???

怎么被吃了???(好吧,先给自己埋个坑,有空补充下 FutureTask 是如何吃掉异常的)

我们尝试下调用 FetureTask 的 get() 方法:

    val futureTask = FutureTask {
        println("futureTask 正在执行")
        throw Exception("我是测试异常")
        return@FutureTask "1"
    }
    Thread(futureTask).start()

    futureTask.get()

运行结果:

异常终于出来了!

所以,对于 FutureTask 的异常操作,除了以上操作,还多了一种方式来进行捕获异常:

    try {
        futureTask.get()
    }catch (e: Exception){
        println("已捕获异常:${e.message}")
    }

为什么说是除了以上操作?是因为 FutureTask 其实只是一个 Runnable 接口的实现类,具体的线程开启还是交给 Thread。

Executors 与 setUncaughtExceptionHandler、ThreadGroup

终于到线程池了!!!

其实,Executors 并没有 setUncaughtExceptionHandler 方法,我这里想表述的是:

  • Thread 可以通过 setUncaughtExceptionHandler 和 ThreadGroup 来进行异常捕获
  • Executors 要执行任务的时候也是要创建线程的,那么,我们可以在创建线程的时候,可以设置setUncaughtExceptionHandler 和 ThreadGroup 来捕获异常。

基本示例如下:(由于逻辑都是大同小异,就不过多说明)

    val executorsService = Executors.newSingleThreadExecutor(object : ThreadFactory {
        override fun newThread(r: Runnable?): Thread {
            val thread = Thread(r)
            thread.setUncaughtExceptionHandler(object : Thread.UncaughtExceptionHandler {
                override fun uncaughtException(t: Thread, e: Throwable) {
                    println("发生异常的线程名字:${t.name}, 异常:${e.message}")
                }
            })
            return thread
        }
    })
    executorsService.execute{
        throw Exception("测试异常")
    }

输出结果:

不过这里有一点需要注意的是,这里是调用 execute 去提交 Runnable 的,但是,假如你使用的是 submit 去提交 Runnable 的话,异常也是会被吃掉的,原理其实就是跟 FutureTask 吃掉异常的原因一样,所以,对于这种情况,可以通过 try catch get() 去捕获异常:

    try {
        future.get()
    }catch (e: Exception){
        println("已捕获异常:${e.message}")
    }

ThreadPoolExecutor 的 afterExecute

这种方式其实并不是捕获异常,而是对异常进行监听。

具体的原理为,ThreadPoolExecutor 在执行任务的时候,会调用 runWorker(Worker w),而 runWorker 大致代码逻辑:

    final void runWorker(Worker w) {
        ...
        Runnable task = w.firstTask;
        ...
        while (task != null || (task = getTask()) != null) {
                ...
            try {
                ...
                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 {
                    ...
            }
        }
        ...
    }

其实,就是对于 run() 进行 try catch 操作,然后把异常存储到 thrown 中,最后在 finally 中执行 afterExecute(task, thrown)

所以,这种方式并不能去捕获异常,但是,当发生异常后,可以在 afterExecute 中获取得到异常信息,并进行自定义处理。

具体的示例代码如下:

    val threadPoolExecutor = object : ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
        LinkedBlockingQueue()) {
        override fun afterExecute(r: Runnable?, t: Throwable?) {
            super.afterExecute(r, t)
            println("异常:${t?.message}")
        }
    }
    threadPoolExecutor.execute {
        throw Exception("测试异常")
    }

运行结果: