JAVA技術分享:消失的執行緒
很多小夥伴都問過我一個問題,就是任務執行緒跑著跑著消失了,而且沒有任何異常日誌。我都是條件反射式的回覆,是不是用了執行緒池的submit提交任務。而且很大機率對方給予肯定答覆。
解決方案,很多人都聽過不少,下面我就分析一下原因以及最佳實踐。
為什麼消失
submit這個單詞用的真的特別好,特別洋氣,雖然可以用execute來提交,但是大部分人都是用的submit。問題也就出在submit上了。
public Future? submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture ftask = newTaskFor(task, null); execute(ftask); return ftask; }
submit也是呼叫的execute,而且是有Future的返回值的。他把一個Runnable 組裝成了一個無返回型別的FutureTask。
FutureTask的run方法與眾不同。
public void run() { .... try { Callable c = callable; if (c != null state == NEW) { V result; boolean ran; try { result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) set(result); } } finally { .... } }
可以看到FutureTask的run自己捕獲了Throwable 而且沒有丟擲,執行緒裡的所有程式碼出了什麼錯都會被捕獲,並且賦值給了成員變數outcome。
最終在get裡呼叫report有一段邏輯可以取出異常
private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s = CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); }
最後異常會在get的時候throw。
程式出錯,異常被吞掉了,最終展現出業務執行緒消失的現象。
解決方案
run裡自己編碼解決異常
自己的程式碼健壯到一言不合來個大寫的try catch 而且一定是Throwable並且要合理的記錄 。寫這種程式碼真心累,而且在一些特殊場合才這麼幹,例如javaagent。搞個探針上去,一堆異常在業務系統上,換誰誰都怕。
future.get()
submit的時候會返回一個Future,我們只要get就可以獲取到異常。這個是一個理論可行的方案,這個也只用在有互動的場景,例如出現異常了再次提交任務等等。再互動的場景下,這個方案特別適合。
execute提交
submit最終也呼叫的execute。execute只能接收引數Runnable。
execute直接把異常給丟擲了,就是不怎麼優雅的記錄。
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); }
每個work執行的時候雖然也會捕獲各種異常,但是最後都throw出來了,也能達到追蹤的效果。
自定義執行緒池
繼承執行緒池然後複寫afterExecute(task, thrown);上面也看到了最後會執行這個方法,而且可以優雅的記錄日誌,以及可以做一些補償操作再裡面。
最佳實踐
一般使用執行緒池都會自己去自定義,畢竟為了區別,我們會自己去寫執行緒工廠去標識自己的執行緒,而且為了記憶體估計會調整阻塞佇列的型別和大小,在一些壓力突增的情況還得控制執行緒池裡最大執行緒的個數與核心執行緒的比率,所以執行緒池的自定義是很有必要的。推薦使用自定義執行緒池的方式記錄日誌。並不推薦補償等邏輯也寫線上程池裡,對於要補償的情況推薦future.get()。這裡只寫異常的處理和補償即可,日誌就不用記錄了。