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