一个线程池 bug 引发的 GC 思考!
問題描述
前幾天在幫同事排查生產(chǎn)一個線上偶發(fā)的線程池錯誤,邏輯很簡單,線程池執(zhí)行了一個帶結(jié)果的異步任務。
但是最近有偶發(fā)的報錯:
java.util.concurrent.RejectedExecutionException:?Task?java.util.concurrent.FutureTask@a5acd19?rejected?from?java.util.concurrent.ThreadPoolExecutor@30890a38\[Terminated,?pool?size?=?0,?active?threads?=?0,?queued?tasks?=?0,?completed?tasks?=?0\]本文中的模擬代碼已經(jīng)問題都是在HotSpot java8 (1.8.0_221)版本下模擬&出現(xiàn)的
下面是模擬代碼,通過Executors.newSingleThreadExecutor創(chuàng)建一個單線程的線程池,然后在調(diào)用方獲取Future的結(jié)果
public?class?ThreadPoolTest?{????public?static?void?main(String[]?args)?{final?ThreadPoolTest?threadPoolTest?=?new?ThreadPoolTest();for?(int?i?=?0;?i?<?8;?i++)?{new?Thread(new?Runnable()?{@Overridepublic?void?run()?{while?(true)?{Future<String>?future?=?threadPoolTest.submit();try?{String?s?=?future.get();}?catch?(InterruptedException?e)?{e.printStackTrace();}?catch?(ExecutionException?e)?{e.printStackTrace();}?catch?(Error?e)?{e.printStackTrace();}}}}).start();}????????//子線程不停gc,模擬偶發(fā)的gcnew?Thread(new?Runnable()?{@Overridepublic?void?run()?{while?(true)?{System.gc();}}}).start();}????/***?異步執(zhí)行任務*?@return*/public?Future<String>?submit()?{//關(guān)鍵點,通過Executors.newSingleThreadExecutor創(chuàng)建一個單線程的線程池ExecutorService?executorService?=?Executors.newSingleThreadExecutor();FutureTask<String>?futureTask?=?new?FutureTask(new?Callable()?{@Overridepublic?Object?call()?throws?Exception?{Thread.sleep(50);return?System.currentTimeMillis()?+?"";}});executorService.execute(futureTask);return?futureTask;}}分析&疑問
第一個思考的問題是:線程池為什么關(guān)閉了,代碼中并沒有手動關(guān)閉的地方。看一下Executors.newSingleThreadExecotor的源碼實現(xiàn):
public?static?ExecutorService?newSingleThreadExecutor()?{return?new?FinalizableDelegatedExecutorService(new?ThreadPoolExecutor(1,?1,?0L,?TimeUnit.MILLISECONDS,?new?LinkedBlockingQueue<Runnable>())); }這里創(chuàng)建的實際上是一個FinalizableDelegatedExecutorService,這個包裝類重寫了finalize函數(shù),也就是說這個類會在被GC回收之前,先執(zhí)行線程池的shutdown方法。
問題來了,GC只會回收不可達(unreachable)的對象,在submit函數(shù)的棧幀未執(zhí)行完出棧之前,executorService應該是可達的才對。
對于此問題,先拋出結(jié)論:
當對象仍存在于作用域(stack frame)時,finalize也可能會被執(zhí)行
oracle jdk文檔中有一段關(guān)于finalize的介紹:
A reachable object is any object that can be accessed in any potential continuing computation from any live thread.
Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable. For example, a Java compiler or code generator may choose to set a variable or parameter that will no longer be used to null to cause the storage for such an object to be potentially reclaimable sooner.
大概意思是:可達對象(reachable object)是可以從任何活動線程的任何潛在的持續(xù)訪問中的任何對象;java編譯器或代碼生成器可能會對不再訪問的對象提前置為null,使得對象可以被提前回收。
也就是說,在jvm的優(yōu)化下,可能會出現(xiàn)對象不可達之后被提前置空并回收的情況
舉個例子來驗證一下(摘自:https://stackoverflow.com/questions/24376768/can-java-finalize-an-object-when-it-is-still-in-scope):
class?A?{@Override?protected?void?finalize()?{System.out.println(this?+?"?was?finalized!");}????public?static?void?main(String[]?args)?throws?InterruptedException?{A?a?=?new?A();System.out.println("Created?"?+?a);for?(int?i?=?0;?i?<?1\_000\_000\_000;?i++)?{if?(i?%?1\_000_00?==?0)System.gc();}System.out.println("done.");} }//打印結(jié)果Created?A@1be6f5c3 A@1be6f5c3?was?finalized! //finalize方法輸出 done.從例子中可以看到,如果a在循環(huán)完成后已經(jīng)不再使用了,則會出現(xiàn)先執(zhí)行finalize的情況;雖然從對象作用域來說,方法沒有執(zhí)行完,棧幀并沒有出棧,但是還是會被提前執(zhí)行。
現(xiàn)在來增加一行代碼,在最后一行打印對象a,讓編譯器/代碼生成器認為后面有對象a的引用,
...System.out.println(a);//打印結(jié)果 Created?A@1be6f5c3 done. A@1be6f5c3從結(jié)果上看,finalize方法都沒有執(zhí)行(因為main方法執(zhí)行完成后進程直接結(jié)束了),更不會出現(xiàn)提前finalize的問題了。
基于上面的測試結(jié)果,再測試一種情況,在循環(huán)之前先將對象a置為null,并且在最后打印保持對象a的引用
A?a?=?new?A(); System.out.println("Created?"?+?a); a?=?null; //手動置null for?(int?i?=?0;?i?<?1\_000\_000\_000;?i++)?{if?(i?%?1\_000_00?==?0)System.gc(); } System.out.println("done."); System.out.println(a); //打印結(jié)果Created? A@1be6f5c3 A@1be6f5c3?was?finalized! done.null從結(jié)果上看,手動置null的話也會導致對象被提前回收,雖然在最后還有引用,但此時引用的也是null了
現(xiàn)在再回到上面的線程池問題,根據(jù)上面介紹的機制,在分析沒有引用之后,對象會被提前finalize
可在上述代碼中,return之前明明是有引用的executorService.execute(futureTask),為什么也會提前finalize呢?
猜測可能是由于在execute方法中,會調(diào)用threadPoolExecutor,會創(chuàng)建并啟動一個新線程,這時會發(fā)生一次主動的線程切換,導致在活動線程中對象不可達。
結(jié)合上面Oracle Jdk文檔中的描述“可達對象(reachable object)是可以從任何活動線程的任何潛在的持續(xù)訪問中的任何對象”,可以認為可能是因為一次顯示的線程切換,對象被認為不可達了,導致線程池被提前finalize了
下面來驗證一下猜想:
//入口函數(shù) public?class?FinalizedTest?{????public?static?void?main(String []?args)?{final?FinalizedTest?finalizedTest?=?new?FinalizedTest();for?(int?i?=?0;?i?<?8;?i++)?{new?Thread(new?Runnable()?{@Overridepublic?void?run()?{while?(true)?{TFutureTask?future?=?finalizedTest.submit();}}}).start();}new?Thread(new?Runnable()?{@Overridepublic?void?run()?{while?(true)?{System.gc();}}}).start();}public?TFutureTask?submit(){TExecutorService?TExecutorService?=?Executors.create();TExecutorService.execute();return?null;} }//Executors.java,模擬juc的Executors public?class?Executors?{/***?模擬Executors.createSingleExecutor*?@return*/public?static?TExecutorService?create(){return?new?FinalizableDelegatedTExecutorService(new?TThreadPoolExecutor());}static?class?FinalizableDelegatedTExecutorService?extends?DelegatedTExecutorService?{FinalizableDelegatedTExecutorService(TExecutorService?executor)?{super(executor);}/***?析構(gòu)函數(shù)中執(zhí)行shutdown,修改線程池狀態(tài)*?@throws?Throwable*/@Overrideprotected?void?finalize()?throws?Throwable?{super.shutdown();}}static?class?DelegatedTExecutorService?extends?TExecutorService?{protected?TExecutorService?e;public?DelegatedTExecutorService(TExecutorService?executor)?{this.e?=?executor;}@Overridepublic?void?execute()?{e.execute();}@Overridepublic?void?shutdown()?{e.shutdown();}} }//TThreadPoolExecutor.java,模擬juc的ThreadPoolExecutorpublic?class?TThreadPoolExecutor?extends?TExecutorService?{/***?線程池狀態(tài),false:未關(guān)閉,true已關(guān)閉*/private?AtomicBoolean?ctl?=?new?AtomicBoolean();@Overridepublic?void?execute()?{//啟動一個新線程,模擬ThreadPoolExecutor.executenew?Thread(new?Runnable()?{@Overridepublic?void?run()?{}}).start();//模擬ThreadPoolExecutor,啟動新建線程后,循環(huán)檢查線程池狀態(tài),驗證是否會在finalize中shutdown//如果線程池被提前shutdown,則拋出異常for?(int?i?=?0;?i?<?1\_000\_000;?i++)?{if(ctl.get()){throw?new?RuntimeException("reject!!!\["+ctl.get()+"\]");}}}@Overridepublic?void?shutdown()?{ctl.compareAndSet(false,true);} }執(zhí)行若干時間后報錯:
Exception?in?thread?"Thread-1"?java.lang.RuntimeException:?reject!!!\[true\]從錯誤上來看,“線程池”同樣被提前shutdown了,那么一定是由于新建線程導致的嗎?
下面將新建線程修改為Thread.sleep測試一下:
//TThreadPoolExecutor.java,修改后的execute方法 public?void?execute()?{try?{//顯式的sleep?1?ns,主動切換線程TimeUnit.NANOSECONDS.sleep(1);}?catch?(InterruptedException?e)?{e.printStackTrace();}//模擬ThreadPoolExecutor,啟動新建線程后,循環(huán)檢查線程池狀態(tài),驗證是否會在finalize中shutdown//如果線程池被提前shutdown,則拋出異常for?(int?i?=?0;?i?<?1\_000\_000;?i++)?{if(ctl.get()){throw?new?RuntimeException("reject!!!\["+ctl.get()+"\]");}} }執(zhí)行結(jié)果一樣是報錯
Exception?in?thread?"Thread-3"?java.lang.RuntimeException:?reject!!!\[true\]由此可得,如果在執(zhí)行的過程中,發(fā)生一次顯式的線程切換,則會讓編譯器/代碼生成器認為外層包裝對象不可達
總結(jié)
雖然GC只會回收不可達GC ROOT的對象,但是在編譯器(沒有明確指出,也可能是JIT)/代碼生成器的優(yōu)化下,可能會出現(xiàn)對象提前置null,或者線程切換導致的“提前對象不可達”的情況。
所以如果想在finalize方法里做些事情的話,一定在最后顯示的引用一下對象(toString/hashcode都可以),保持對象的可達性(reachable)
上面關(guān)于線程切換導致的對象不可達,沒有官方文獻的支持,只是個人一個測試結(jié)果,如有問題歡迎指出
綜上所述,這種回收機制并不是JDK的bug,而算是一個優(yōu)化策略,提前回收而已;但Executors.newSingleThreadExecutor的實現(xiàn)里通過finalize來自動關(guān)閉線程池的做法是有Bug的,在經(jīng)過優(yōu)化后可能會導致線程池的提前shutdown,從而導致異常。
線程池的這個問題,在JDK的論壇里也是一個公開但未解決狀態(tài)的問題https://bugs.openjdk.java.net/browse/JDK-8145304。
不過在JDK11下,該問題已經(jīng)被修復:
JUC??Executors.FinalizableDelegatedExecutorServicepublic?void?execute(Runnable?command)?{????try?{e.execute(command);}?finally?{?reachabilityFence(this);?} }作者:空無
https://segmentfault.com/a/1190000021109130
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的一个线程池 bug 引发的 GC 思考!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 陆金所 CAT 优化实践
- 下一篇: 计算机网络基础知识,仅此一篇足矣