被開發(fā)者拋棄的 Executors,錯(cuò)在哪兒?
一、序
在 Java 領(lǐng)域內(nèi),我們使用多線程的方式來實(shí)現(xiàn)并發(fā)編程。而線程本身是操作系統(tǒng)的一個(gè)概念,雖然不同的語言對線程都進(jìn)行了一些封裝,但是最終都是調(diào)用到操作系統(tǒng)中去創(chuàng)建和調(diào)度線程。
既然線程是一項(xiàng)重要的系統(tǒng)資源,為了更合理的利用此資源,我們會(huì)使用池化技術(shù)來優(yōu)化線程的創(chuàng)建和銷毀,這就是線程池。
在我們學(xué)習(xí)并發(fā)編程的時(shí)候,線程可以利用 Thread 來創(chuàng)建并通過 start() 來啟動(dòng)一個(gè)線,但在成熟的項(xiàng)目中,基本上是不允許這樣操作線程的,都需要通過線程池去收斂線程的使用,所以線程池是必須的。
Java 的線程池可以通過 ThreadPoolExecutor 來構(gòu)造,在其中提供非常完備的構(gòu)造方法,可以根據(jù)我們的業(yè)務(wù)需求靈活的構(gòu)造線程池。同時(shí) Java 還提供了一個(gè) Executors,它內(nèi)部提供了很多包裝的方法,利用它可以幫我們快速的構(gòu)建線程池。
原本 Executors 的目的就是為了讓我們更方便的使用線程池,但是《阿里巴巴Java開發(fā)手冊》也明確指出,直接使用 Executors 的缺陷。
手冊中提到強(qiáng)制不允許使用 Executors 去創(chuàng)建線程池,而是應(yīng)該使用退化到最原始的 ThreadPoolExecutor 的方式。
日常開發(fā)中,應(yīng)該收緊對線程池的創(chuàng)建,由開發(fā)人員明確線程池的運(yùn)行規(guī)則,以此來盡量規(guī)避其資源耗盡的風(fēng)險(xiǎn)。
線程池是個(gè)好東西,但是怎么創(chuàng)建是一個(gè)問題。
二、Executors 怎么了?
1. 不被允許的 Executors
不應(yīng)該使用 Executors 的原因,其實(shí)《阿里巴巴Java開發(fā)手冊》里已經(jīng)寫明了,當(dāng)需要處理大量任務(wù)的時(shí)候,可能會(huì)出現(xiàn) OOM 異常,但它們出現(xiàn) OOM 的原因并不一樣。
ThreadPoolExecutor 的構(gòu)造方法中,提供了很多參數(shù)的配置,其中與 Executors 出現(xiàn) OOM 相關(guān)的就有 2 個(gè):核心線程數(shù)和等待隊(duì)列。
先來看看 FixedThreadPool 和 SingleThreadPool 出現(xiàn) OOM 的原因。
它們的問題在于等待隊(duì)列使用了 LinkedBlockingQueue 這個(gè)以鏈表實(shí)現(xiàn)的無界隊(duì)列(最大長度是 Integer.MAX_VALUE),最終導(dǎo)致堆積了大量等待處理的任務(wù),從而導(dǎo)致頻繁的 GC,最終觸發(fā) OOM。
- java.lang.OutOfMemoryError: GC overhead limit exceeded
再來看看 CachedThreadPool 出現(xiàn) OOM 的原因。
它的問題在于核心線程數(shù)設(shè)置為了 Integer.MAX_VALUE,并且等待隊(duì)列是一個(gè) SynchronousQueue。
SynchronousQueue 是一個(gè)沒有數(shù)據(jù)緩沖的阻塞隊(duì)列,它極易被阻塞。在等待隊(duì)列被阻塞的時(shí)候,如果線程數(shù)量還沒有達(dá)到核心線程數(shù)限制的數(shù)量時(shí),線程池的策略是創(chuàng)建新的線程來處理新的任務(wù)。
也就是說,是核心線程數(shù)和等待隊(duì)列 SynchronousQueue 合力造成了線程會(huì)跟隨任務(wù)不斷的被創(chuàng)建,直到觸發(fā) OOM。
- java.lang.OutOfMemoryError: pthread_creat (1040KB stack) failed: Try again
ScheduledThreadPool 的等待隊(duì)列使用的是 DelayedWorkQueue,原理也是類似的,最終會(huì)導(dǎo)致創(chuàng)建大量的線程而拋出 OOM。
線程是一種系統(tǒng)資源,本身創(chuàng)建就會(huì)帶來內(nèi)存開銷,同時(shí)操作系統(tǒng)對單進(jìn)程可創(chuàng)建的線程數(shù)也是有限制的。
在 Android 中,每個(gè)線程初始化都需要 mmap 一定的堆內(nèi)存,在默認(rèn)的情況下,初始化一個(gè)線程大約需要 mmap 1MB 左右的內(nèi)存空間。同時(shí)系統(tǒng)本身也會(huì)對每個(gè)進(jìn)程可創(chuàng)建的線程數(shù),做一定的限制,這個(gè)限制在 /proc/pid/limits 中,不同的廠商對這個(gè)限制也有所不同,當(dāng)超出限制時(shí),哪怕堆上還有可用內(nèi)存,依然會(huì)拋出 OOM。
2. Executors 錯(cuò)在哪兒了?
Executors 會(huì)在任務(wù)過多的時(shí)候,導(dǎo)致資源耗盡而觸發(fā) OOM,這是它帶來的危害。
Executors 最大的問題,在于沒有邊界。
在系統(tǒng)環(huán)境良好,任務(wù)不多的時(shí)候 Executors 創(chuàng)建的線程池,都是可以正常工作的。
但是一旦有重壓,我們就無法預(yù)知什么時(shí)候會(huì)出現(xiàn)問題,這就是沒有邊界,沒有邊界就意味著不可控。
我們很難去信任一段不可控的代碼,它什么時(shí)候出現(xiàn)問題,完全是不可預(yù)知的,這才是 Executors 最大的問題。
除此之外,Executors 封裝了太多線程池的細(xì)節(jié),本身也不建議使用。例如通常我們需要給線程池創(chuàng)建的線程,起一個(gè)有意義的名稱,方便在出現(xiàn)異常的時(shí)候排查問題;再例如對與線程池的拒絕策略,我們需要深思熟慮的定義,是直接拋棄還是持久化下來延遲處理。
去思考一個(gè)線程池的不同參數(shù)帶來的策略細(xì)節(jié),才是使用線程池的一個(gè)良好的開發(fā)習(xí)慣。
三. 小結(jié)時(shí)刻
本文我們聊了關(guān)于創(chuàng)建線程池,使用 Executors 創(chuàng)建的線程池會(huì)有 OOM 的風(fēng)險(xiǎn),應(yīng)該使用 ThreadPoolExecutor 去創(chuàng)建線程池。通過思考業(yè)務(wù)來明確配置線程池不同的參數(shù),例如線程池、等待隊(duì)列、拒絕策略等等。