說說你對線程池的理解?java
首先明確,池化的意義在於緩存,建立性能開銷較大的對象,好比線程池、鏈接池、內存池。預先在池裏建立一些對象,使用時直接取,用完就歸還複用,使用策略調整池中緩存對象的數量。數據庫
Java建立對象,僅是在JVM堆分塊內存,但建立一個線程,卻需調用os內核API,而後os要爲線程分配一系列資源,成本很高,因此線程是一個重量級對象,應避免頻繁建立或銷燬。 既然這麼麻煩,就要避免呀,因此要使用線程池!緩存
通常池化資源,當你須要資源時,就調用申請線程方法申請資源,用完後調用釋放線程方法釋放資源。但JDK的線程池根本沒有申請線程和釋放線程的方法。bash
那到底該如何理解它的設計思想呢? 其實線程池的設計,採用的是生產者-消費者模式:markdown
如下簡化代碼便可顯示線程池的基本原理: JDK線程池最核心的就是ThreadPoolExecutor,看名字,它強調的是Executor,並不是通常的池化資源。多線程
爲何都說要手動聲明線程池?併發
雖然JDK的Executors
工具類提供的方法可快速建立線程池。 但阿里有話說:
異步
弊端真的這麼嚴重嗎,newFixedThreadPool=OOM?工具
寫段測試代碼: 性能
執行不久,出現OOM
Exception in thread "http-nio-30666-ClientPoller"
java.lang.OutOfMemoryError: GC overhead limit exceeded
複製代碼
newFixedThreadPool
線程池的工做隊列直接new了一個LinkedBlockingQueueInteger.MAX_VALUE
長度的隊列,因此很快Q滿雖然使用newFixedThreadPool
能夠固定工做線程數量,但任務隊列幾乎無界。若任務較多且執行較慢,隊列就會快速積壓,內存不夠,易致使OOM。
newCachedThreadPool也等於OOM?
[11:30:30.487] [http-nio-30666-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause
java.lang.OutOfMemoryError: unable to create new native thread
複製代碼
可見OOM是由於沒法建立線程,newCachedThreadPool這種線程池的最大線程數是Integer.MAX_VALUE,也可認爲無上限,而其工做隊列SynchronousQueue是一個沒有存儲空間的阻塞隊列。 因此只要有請求到來,就必須找到一條工做線程處理,若當前無空閒線程就再建立一個新的。 因爲咱們的任務需很長時間才能執行完成,大量任務進來後會建立大量線程。而線程是須要分配必定內存空間做爲線程棧的,好比1MB,所以無限建立線程必OOM
因此使用線程池,請不要抱任何僥倖,覺得只是處理輕量任務,不會形成隊列積壓或建立大量線程! 好比某業務一旦接受到請求,就會調用外部服務,該外部服務接口正常100ms內會響應,如今TPS過百,CachedThreadPool能穩定在佔用10個左右線程狀況下知足需求。 可天有不測風雲,該外部服務不可用了!而代碼裏調用該服務設置的超時又特別長, 好比1min,1min可能已經進成千上萬請求,產生幾千個任務,需幾千個線程,沒多久就由於沒法再建立新線程,OOM!
因此阿里纔不建議使用Executors:
由於當出現線程數量暴增、死鎖、CPU負載高、線程執行異常等事故時,每每都需抓取線程棧。有意義的線程名稱,就很重要。示例以下:
注意異常處理
經過ThreadPoolExecutor#execute()提交任務時,若任務在執行的過程當中出現運行時異常,會致使 執行任務的線程 終止。 但要命的是,有時任務雖然異常了,但你卻收不到任何通知,你還在開心摸魚,覺得任務都執行很正常。雖然線程池提供了不少用於異常處理的方法,但最穩妥和簡單的方案仍是捕獲全部異常並具體處理:
線程池的線程管理
還好有谷歌,通常咱們直接利用guava的ThreadFactoryBuilder實現線程池線程的自定義命名便可。
線程池的拒絕策略
線程池默認的拒絕策略會拋RejectedExecutionException,這是個運行時異常,IDEA不會強制捕獲,因此咱們也很容易忽略它。 對於採用何種策略,具體要看任務的重要性:
如果一些不重要任務,可選擇直接丟棄
重要任務,可採用降級,好比將任務信息插入DB或MQ,啓用一個專門用做補償的線程池去補償處理。所謂降級,也就是在服務沒法正常提供功能的狀況下,採起的補救措施。具體處理方式也看具體場景而不一樣。
當線程數大於核心線程數時,線程等待keepAliveTime後仍是無任務須要處理,收縮線程到核心線程數
瞭解這個策略,有助於咱們根據實際的容量規劃需求,爲線程池設置合適的初始化參數。也可經過一些手段來改變這些默認工做行爲,好比:
彈性伸縮的實現
線程池是先用Q存放來不及處理的任務,滿後再擴容線程池。當Q設置很大時(那個 工具類),最大線程數這個參數就沒啥意義了,由於隊列很難滿或到滿時可能已OOM,更沒機會去擴容線程池了。 是否能讓線程池優先開啓更多線程,而把Q當成後續方案?好比咱們的任務執行很慢,須要10s,若線程池可優先擴容到5個最大線程,那麼這些任務最終均可以完成,而不會由於線程池擴容過晚致使慢任務來不及處理。
難題在於:
重寫隊列的offer,人爲製造該隊列滿的條件
實現一個自定義拒絕策略,這時再把任務真正插入隊列
Tomcat就實現了相似的「彈性」線程池。
務必確認清楚線程池自己是否是複用的。
某服務偶爾報警線程數過多,但過一下子又會降下來,但應用的請求量卻變化不大。
能夠在線程數較高時抓取線程棧,發現內存中有上千個線程池,這確定不正常!
但代碼裏也沒看到聲明瞭線程池,最後發現原來是業務代碼調用了一個類庫: 該類庫居然每次都建立一個新的線程池!
newCachedThreadPool會在須要時建立必要數量的線程,業務代碼的一次業務操做會向線程池提交多個慢任務,這樣執行一次業務操做就會開啓多個線程。若是業務操做併發量較大的話,的確有可能一會兒開啓幾千個線程。
那爲什麼監控中看到線程數量會降低,而不OOM?
newCachedThreadPool的核心線程數是0,而keepAliveTime是60s,因此60s後全部線程均可回收。
那這如何修復呢?
使用static字段存放線程池引用便可
線程池的意義在於複用,就意味着程序應該始終使用一個線程池嗎?
不,具體場景具體分析。
好比一個 I/O 型任務,不斷向線程池提交任務:向一個文件寫入大量數據。線程池的線程基本一直處於忙碌狀態,隊列也基本滿。並且因爲是CallerRunsPolicy策略,因此當線程滿隊列滿,任務會在提交任務的線程或調用execute方法的線程執行,因此不要認爲提交到線程池的任務就必定會被異步處理。
畢竟,若使用CallerRunsPolicy,就有可能異步任務變同步執行。使用CallerRunsPolicy,當線程池飽和時,計算任務會在執行Web請求的Tomcat線程執行,這時就會進一步影響到其餘同步處理的線程,甚至形成整個應用程序崩潰。
如何修正?
使用單獨的線程池處理這種「I/O型任務」,將線程數設置多一些!
因此千萬不要盲目複用別人寫的線程池!由於它不必定適合你的任務!
Java 8的parallel stream
可方便並行處理集合中的元素,共享同一ForkJoinPool,默認並行度是CPU核數-1。對於CPU綁定的任務,使用這樣的配置較合適,但若集合操做涉及同步I/O操做(好比數據庫操做、外部服務調用),建議自定義一個ForkJoinPool(或普通線程池)。
最後聲明一點:提交到相同線程池中的任務,必定要是相互獨立的,最好不要有依賴關係!
參考
- 《阿里巴巴Java開發手冊》