事故案例分享:線程池引發的app crush

問題描述

為安卓應用提供了一個SDK,由於初始化操作比較耗時,因此SDK在初始化方法內部利用多線程並行進行初始化。

由於某些原因導致初始化時開啟的某個線程拋出了異常,這個異常並不能被上層捕獲導致app crush;

代碼的大體邏輯如下:

<code>        ExecutorService executors = Executors.newFixedThreadPool(4);  //app 本身的線程池
executors.execute(() -> {//執行初始化方法
//模擬SDK的初始化方法
ExecutorService internalExecutors = Executors.newFixedThreadPool(4);
internalExecutors.execute(() -> {
throw new RuntimeException("拋個異常。");
});
internalExecutors.shutdown();
while (!internalExecutors.isTerminated()) {
}
});/<code>
事故案例分享:線程池引發的app crush

問題分析

拋出異常的思路是沒有問題的,因為sdk沒有合適的方式處理這個異常,需要通知上層此時的SDK是不能工作的;

主要問題是這個異常拋出去了,但並沒有拋給方法的調用者,而是拋出到另一個線程。

所以應該解決的問題是將異常拋給初始化方法的調用者;


事故案例分享:線程池引發的app crush


解決方案

方案一:

將SDK內部的多線程改為單線程,這樣可以很自然的將異常拋給“初始化方法”的調用者;但會增加一定的耗時;

方案二:

仍然利用線程池並行處理,但是確保線程池使用excute執行的任務不會拋出異常;

前面也說了,拋出異常的思路是沒有問題的,那麼在發生異常時就需要處理並保存異常,這個異常還應該可以被初始化方法的主線程獲取到,然後拋給調用者;

如果讓每個task來處理自己的異常,顯然會造成代碼的冗餘,而且一旦異常處理邏輯需要修改,則可能會涉及到多出修改,所以最好有一種類似“切面”的東西來處理異常;

具體方案如下:

<code>    /**
* 自定義線程工廠,記錄異常(根據我們的實際場景,只記錄一個即可)

*/
static class ExceptionRecordThreadFactory implements ThreadFactory {
private volatile Throwable exception = null;

@Override
public Thread newThread(Runnable r) {
Thread th = new Thread(r);
th.setUncaughtExceptionHandler((t, e) -> {
if (exception == null) {
synchronized (ExceptionRecordThreadFactory.class) {
if (exception == null) {
exception = e;
}
}
}
System.out.println(t.getName() + "\\t" + e.getMessage());
});
return th;
}

public Throwable getException() {
return exception;
}
}/<code>


創建線程池時,使用這個線程池所需要執行的線程都通過線程工廠來創建;

<code>      ExecutorService executors = Executors.newFixedThreadPool(4);//app 本身的線程池
executors.execute(() -> {//執行初始化方法
//模擬SDK的初始化方法
ExceptionRecordThreadFactory threadFactory = new ExceptionRecordThreadFactory();
ExecutorService internalExecutors = Executors.newFixedThreadPool(4, threadFactory);
internalExecutors.execute(() -> {
throw new RuntimeException("拋個異常。");
});
internalExecutors.shutdown();
while (!internalExecutors.isTerminated()) {
}
System.out.println("exception info:" + (threadFactory.getException() == null ? "未發現異常" : threadFactory.getException().getMessage()));
});/<code>

其他方面分析

由於我們的目的很簡單,就是需要7、8個任務併發執行,縮短初始化時間;

因此直接使用JDK提供的Executors創建固定線程池,但一般情況下不推薦這樣創建線程池,具體理由在後面的文章中分析;

另外線程池是創建在方法內部的,方法執行完之後就會GC,但一般來講都是全局設置一個線程池;從這個維度來講,也並不是很合理,這是由於整體架構導致的,負責SDK的並沒有全局的上下文;

為什麼使用線程池,而不是單獨啟動7、8個線程,因為測試中發現並不是線程數越多越好,一般設置為CPU核數的1/2時效果最好;


事故案例分享:線程池引發的app crush


分享到:


相關文章: