线程的异常捕获引发的一些思考

798 阅读4分钟

背景

本次异常的发生是因为一次代码重构,众所周知Android调用线程捕获不了子线程抛出的异常,而子线程异常没有捕获处理的话会导致整个App Crash。

  • 更改前
val singleExecutorService = Executors.newSingleThreadExecutor()

fun submit(runnable: () -> Unit) {
    try {
        singleExecutorService.submit {
            runnable()
        }
    } catch (t: Throwable) {
        // 异常处理
    }
}

从上述代码可以看到调用线程将runnable执行任务通过submit抛到子线程,同时使用try...catch进行异常捕获,当然try...catch肯定是不生效的。

这里可能会有疑问,如果该代码不能捕获子线程异常,那为什么没有发生崩溃呢?

这是因为线程池的submit方法会将提交的runnable封装成一个FutureTask,然后再执行execute方法,最终会执行FutureTask的run方法,该方法会在内部捕获异常并保存,外部通过调用FutureTask的get方法获取异常。

image

  • 更改后

为了对子线程统一进行异常捕获处理,在定义单线程池的方法中传入threadFactory,自定义thread异常处理uncaughtExceptionHandler,自以为可以在uncaughtExceptionHandler中捕获并处理该thread中抛出的所有异常。

val singleExecutorService: Executor by lazy {
    Executors.newSingleThreadExecutor { r ->
        val thread = Thread(r)
        thread.name = "xxx-thread"
        thread.priority = Thread.NORM_PRIORITY - 3
        thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { _, e ->
            // 异常处理
        }
        thread
    }
}

fun execute(runnable: () -> Unit) {
    singleExecutorService.execute(runnable)
}
  • 代码上线

结果万万没想到代码上线后在灰度期间Slardar就报了几十个Crash,而且随着灰度放量Crash量也在迅速上升,大有阻塞App发灰的节奏

源码分析

从uncaughtExceptionHandler源码的定义可以看到从JDK 1.5开始,当一个线程因为没有捕获异常而即将终止时,Java虚拟机会调用该线程的uncaughtExceptionHandler处理异常,如果线程没有自定义则往上查找线程的ThreadGroup异常处理器进行处理。

image

原因追溯

百思不得其解,不应该呀,看源码注释设置uncaughtExceptionHandler确实是给线程自定义异常处理,而且自测确实也调用了Thread.UncaughtExceptionHandler异常处理方法uncaughtException,App也没有崩溃,那为什么会触发Slardar异常上报呢?

后来发现异常崩溃时除了调用uncaughtException,同时还打印了系统异常堆栈,怀疑误触发了Slardar异常上报,而且不断调用threadFactory的newThread方法创建线程。

这个就有点诡异了,既然定义的单线程池,为什么发生崩溃的时候会创建新的线程呢,那原来的线程去哪了?

既然是线程池中线程的异常捕获,接下来只能去线程池源码中一探究竟。线程池中线程的执行会调用runWorker方法,如果在执行的过程中有异常发生最终会执行finally中的processWorkerExit(w, completedAbruptly)

image

processWorkerExit的具体执行逻辑为

image

可以看到线程池中线程在执行过程中发生异常会把该线程移除掉然后创建一个新的线程,所以不推荐使用uncaughtExceptionHandler来处理线程异常,在高并发场景可能会频繁创建和销毁。

  • 小结

到这里真相大白,uncaughtExceptionHandler虽然可以自定义异常处理,但是同时也会打印系统异常堆栈,从而误触发Slardar崩溃上报,虽然App实际上可能并没有崩溃。同时uncaughtExceptionHandler并不能防止线程崩溃,只不过在线程崩溃后线程池会迅速重新创建一个新的线程放回线程池,所以在高并发场景不推荐使用。

最终解决方案

去掉uncaughtExceptionHandler,直接用kotlin.runCatching包装runnable组成一个新的runnable提交到子线程执行,这样我们就可以在子线程捕获异常并处理了,保证子线程不会崩溃重建。

val singleExecutorService: ExecutorService by lazy {
    Executors.newSingleThreadExecutor { r ->
        val thread = Thread(r)
        thread.name = "xxx-thread"
        thread.priority = Thread.NORM_PRIORITY - 3
        thread

    }
}

fun execute(runnable: () -> Unit) {
    singleExecutorService.execute {
        kotlin.runCatching {
            runnable()
        }.onFailure {
            // 异常处理
        }
    }
}

写在最后

有时候我们知其然还要知其所以然,不能想当然去重构一些代码,可能在解决一些问题的同时又带来新问题。有时间可以多看看Android源码,里面有一些设计非常巧妙,值得我们深入学习。