我們一起深入多線程面試連環炮
1、什么是線程池
線程的創建和銷毀是一個“重”操作,所以我們需要避免線程頻繁地創建與銷毀,因此我們需要緩存一批線程,讓它們時刻準備著執行任務
目標已經很清晰了,弄一個池子,里面存放約定數量的線程,這就是線程池,一種池化技術
如果線程數太少無法充分利用 CPU ,太多的話由于上下文切換的消耗又得不償失,所以我們需要評估系統所要承載的并發量和所執行任務的特性,得出大致需要多少個線程數才能充分利用 CPU,因此需要控制線程數量
多線程技術主要解決處理器單元內多個線程執行的問題,它可以顯著減少處理器單元的閑置時間,增加處理器單元的吞吐能力
假設一個服務器完成一項任務所需時間為:T1 創建線程時間,T2 在線程中執行任務的時間,T3 銷毀線程時間
如果:T1 + T3 遠大于 T2,則可以采用線程池,以提高服務器性能
線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者一些空閑的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了
線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目
2、線程池優點,為什么要使用線程池
new Thread 缺點
每次new Thread新建對象性能差
線程缺乏統一管理,可能無限制新建線程,相互之間競爭,及可能占用過多系統資源導致死機或oom
缺乏更多功能,如定時執行、定期執行、線程中斷
為什么要用線程池
減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務。
可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)。
ThreadPool優點
減少了創建和銷毀線程的次數,每個工作線程都可以被重復利用,可執行多個任務
可以根據系統的承受能力,調整線程池中工作線線程的數目,防止因為因為消耗過多的內存,而把服務器累趴下(每個線程需要大約1MB內存,線程開的越多,消耗的內存也就越大,最后死機)
減少在創建和銷毀線程上所花的時間以及系統資源的開銷
如不使用線程池,有可能造成系統創建大量線程而導致消耗完系統內存
3、常用的線程池
第1種是:固定大小線程池,特點是線程數固定,使用無界隊列,適用于任務數量不均勻的場景、對內存壓力不敏感,但系統負載比較敏感的場景
第2種是:Cached線程池,特點是不限制線程數,適用于要求低延遲的短期任務場景
第3種是:單線程線程池,也就是一個線程的固定線程池,適用于需要異步執行但需要保證任務順序的場景
第4種是:Scheduled線程池,適用于定期執行任務場景,支持按固定頻率定期執行和按固定延時定期執行兩種方式
第5種是:工作竊取線程池,使用的ForkJoinPool,是固定并行度的多任務隊列,適合任務執行時長不均勻的場景
4、聽說過Executors嗎
Java里面線程池的頂級接口是Executor,但是嚴格意義上講Executor并不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是ExecutorService
Executors是一個工具類,類里面提供了一些靜態工廠,生成一些常用的線程池
Executors提供四種線程池
newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程
newFixedThreadPool 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待
newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行
一般都不用Executors提供的線程創建方式,使用ThreadPoolExecutor創建線程池
5、那你說說為什么阿里巴巴不建議使用Executors靜態工廠構建線程池
在阿里巴巴Java開發手冊中提到,使用Executors創建線程池可能會導致OOM(OutOfMemory ,內存溢出),真正的導致OOM的其實是LinkedBlockingQueue.offer方法
底層是通過LinkedBlockingQueue實現的, LinkedBlockingQueue是一個用鏈表實現的有界阻塞隊列,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE
問題就出在:不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設置LinkedBlockingQueue的容量的話,其默認容量將會是Integer.MAX_VALUE
對于一個無邊界隊列來說,是可以不斷的向隊列中加入任務的,這種情況下就有可能因為任務過多而導致內存溢出問題
避免使用Executors創建線程池,主要是避免使用其中的默認實現,那么我們可以自己直接調用ThreadPoolExecutor的構造函數來自己創建線程池。在創建的同時,給BlockQueue指定容量就可以了
6、線程池核心參數有哪些
第1個參數:設置核心線程數。默認情況下核心線程會一直存活
第2個參數:設置最大線程數。決定線程池最多可以創建的多少線程
第3個參數和第4個參數:用來設置線程空閑時間,和空閑時間的單位,當線程閑置超過空閑時間就會被銷毀。可以通過AllowCoreThreadTimeOut方法來允許核心線程被回收
第5個參數:設置緩沖隊列,圖中左下方的三個隊列是設置線程池時常使用的緩沖隊列
其中Array Blocking Queue是一個有界隊列,就是指隊列有最大容量限制。Linked Blocking Queue是無界隊列,就是隊列不限制容量。最后一個是Synchronous Queue,是一個同步隊列,內部沒有緩沖區
第6個參數:設置線程池工廠方法,線程工廠用來創建新線程,可以用來對線程的一些屬性進行定制,例如線程的Group、線程名、優先級等。一般使用默認工廠類即可
第7個參數:設置線程池滿時的拒絕策略
ThreadPoolExecutor默認有四個拒絕策略:
ThreadPoolExecutor.AbortPolicy() 直接拋出異常RejectedExecutionException,這個是默認的拒絕策略
ThreadPoolExecutor.CallerRunsPolicy() 直接在提交失敗時,由提交任務的線程直接執行提交的任務
ThreadPoolExecutor.DiscardPolicy() 直接丟棄后來的任務
ThreadPoolExecutor.DiscardOldestPolicy() 丟棄在隊列中最早提交的任務
7、線程池的工作原理
我們向線程提交任務時可以使用Execute和Submit,區別就是Submit可以返回一個Future對象,通過Future對象可以了解任務執行情況,可以取消任務的執行,還可獲取執行結果或執行異常。Submit最終也是通過Execute執行的
線程池提交任務時的執行順序如下:
向線程池提交任務時,會首先判斷線程池中的線程數是否大于設置的核心線程數,如果不大于,就創建一個核心線程來執行任務
如果大于核心線程數,就會判斷緩沖隊列是否滿了,如果沒有滿,則放入隊列,等待線程空閑時執行任務
如果隊列已經滿了,則判斷是否達到了線程池設置的最大線程數,如果沒有達到,就創建新線程來執行任務
如果已經達到了最大線程數,則執行指定的拒絕策略。這里需要注意隊列的判斷與最大線程數判斷的順序,不要搞反
如果你提交任務時,線程池隊列已滿,這時會發生什么?
如果你使用的LinkedBlockingQueue,也就是無界隊列的話,沒關系,繼續添加任務到阻塞隊列中等待執行,因為LinkedBlockingQueue可以近乎認為是一個無窮大的隊列,可以無限存放任務
如果你使用的是有界隊列比方說ArrayBlockingQueue的話,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy
8、高并發、任務執行時間短的業務怎樣使用線程池?并發不高、任務執行時間長的業務怎樣使用線程池?并發高、業務執行時間長的業務怎樣使用線程池?
高并發、任務執行時間短的業務,線程池線程數可以設置為CPU核數+1,減少線程上下文的切換
并發不高、任務執行時間長的業務要分情況來討論
假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大線程池中的線程數目,讓CPU處理更多的業務
假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,線程數設置為CPU核數+1,線程池中的線程數設置得少一些,減少線程上下文的切換
并發高、業務執行時間長,解決這種類型任務的關鍵不在于線程池而在于整體架構的設計,看看這些業務里面某些數據是否能做緩存是第一步,增加服務器是第二步,至于線程池的設置,參考上面的設置即可
最后,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦
9、聽說過ThreadLocal嗎
看我的這一篇介紹
10、簡單介紹下阻塞隊列吧
阻塞隊列是一個在隊列基礎上又支持了兩個附加操作的隊列
支持阻塞的插入方法:隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。支持阻塞的移除方法:隊列空時,獲取元素的線程會等待隊列變為非空
阻塞隊列的應用場景
阻塞隊列常用于生產者和消費者的場景,生產者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。簡而言之,阻塞隊列是生產者用來存放元素、消費者獲取元素的容器
1、ArrayBlockingQueue 數組結構組成的有界阻塞隊列
此隊列按照先進先出(FIFO)的原則對元素進行排序,但是默認情況下不保證線程公平的訪問隊列,即如果隊列滿了,那么被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
2、LinkedBlockingQueue一個由鏈表結構組成的有界阻塞隊列,此隊列按照先出先進的原則對元素進行排序
3、PriorityBlockingQueue支持優先級的無界阻塞隊列
4、DelayQueue支持延時獲取元素的無界阻塞隊列,即可以指定多久才能從隊列中獲取當前元素
5、SynchronousQueue不存儲元素的阻塞隊列,每一個put必須等待一個take操作,否則不能繼續添加元素。并且支持公平訪問隊列。
6、LinkedTransferQueue由鏈表結構組成的無界阻塞TransferQueue隊列。相對于其他阻塞隊列,多了tryTransfer和transfer方法
transfer方法
如果當前有消費者正在等待接收元素(take或者待時間限制的poll方法),transfer可以把生產者傳入的元素立刻傳給消費者。如果沒有消費者等待接收元素,則將元素放在隊列的tail節點,并等到該元素被消費者消費了才返回
tryTransfer方法
用來試探生產者傳入的元素能否直接傳給消費者。,如果沒有消費者在等待,則返回false。和上述方法的區別是該方法無論消費者是否接收,方法立即返回。而transfer方法是必須等到消費者消費了才返回
11、LinkedBlockingDeque鏈表結構的雙向阻塞隊列,優勢在于多線程入隊時,減少一半的競爭
本文轉載自微信公眾號「Java賊船」,可以通過以下二維碼關注。轉載本文請聯系Java賊船公眾號。