Java面試系列之并發(fā)編程專題-Java線程池靈魂拷問
作者:
修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
金三銀四跳槽季即將來臨,想必有些猿友已經(jīng)蠢蠢欲動在做相關(guān)的準(zhǔn)備了!在接下來的日子里,筆者將堅持寫作、分享Java工程師在面試求職期間的方方面面,包括簡歷制作、面試場景復(fù)現(xiàn)、面試題解答、談薪技巧 以及 項目的實戰(zhàn)!今天我們來聊聊Java中線程池相關(guān)的知識!
在上一篇文章中我們模擬了一個面試場景,靈魂式拷問了Java中Synchronized相關(guān)的知識,可以點擊鏈接查看詳情:Java面試系列之并發(fā)編程專題-Synchronized靈魂拷問
下面我們開擼!值得一提的是,以下內(nèi)容來自程序員實戰(zhàn)基地fightjava.com一位網(wǎng)友最近的面試場景,筆者嘗試著將其復(fù)現(xiàn),其中有些是筆者自行整理補充的,若有不對的地方還請多多指正!!
1. 面試官:Java線程池底層是怎么實現(xiàn)的?大概說下
(1)畫外音:TND的,一上來就問底層實現(xiàn)原理……真應(yīng)了那句“面試造火箭,工作擰螺絲”,沒辦法只能硬著頭皮上,既然說“大概說下”,那大概能回答 “工作線程隊列”和“任務(wù)隊列”應(yīng)該就闊以了!
(2)回答:在Java中,所謂的線程池中的“線程”,其實是被抽象為一個靜態(tài)內(nèi)部類Worker,即“工作線程”,它基于AQS(抽象隊列同步器)實現(xiàn)、存放在線程池一個成員變量中,其名為:“工作線程隊列” HashSet<Worker> workers,而將等待被執(zhí)行的任務(wù)存放在成員變量 “任務(wù)隊列” workQueue(BlockingQueue<Runnable> workQueue)中;
這樣一來,整個線程池實現(xiàn)的基本思想大概就是:從任務(wù)隊列workQueue中不斷取出需要執(zhí)行的任務(wù),放在工作線程隊列Workers中進(jìn)行處理;
2.面試官:嗯,不錯,說一說創(chuàng)建線程池的幾個核心構(gòu)造參數(shù)?
(1)畫外音:這個倒不難,擼過ThreadPoolExecutor的估計都曉得!
(2)回答: Java中創(chuàng)建線程池其實非常靈活,我們可以通過配置不同的參數(shù),創(chuàng)建出行為不同的線程池,這幾個參數(shù)包括:
A. corePoolSize:線程池的核心線程數(shù);
B. maximumPoolSize:線程池允許的最大線程數(shù);
C. keepAliveTime:超過核心線程數(shù)時閑置線程的存活時間;
D. workQueue:任務(wù)執(zhí)行前保存任務(wù)的隊列,保存著execute方法待提交的Runnable任務(wù);
附上代碼:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
3.面試官:那線程池中的線程是怎么創(chuàng)建的?是一開始就隨著線程池的啟動就創(chuàng)建好的嗎?
(1)畫外音:這還是挺考驗底層源碼閱讀能力的,看過ThreadPoolExecutor的創(chuàng)建、executor下的執(zhí)行方法API 即execute()方法的應(yīng)該可以回答上!
(2)回答:不是;線程池在創(chuàng)建后執(zhí)行初始化策略時默認(rèn)是不啟動工作線程Worker的,而是等待有請求到來時才啟動,每當(dāng)我們調(diào)用execute()方法添加一個任務(wù)時,線程池會做如下判斷:
A.如果正在運行的線程數(shù)量小于corePoolSize,那么馬上創(chuàng)建線程運行這個任務(wù);如果正在運行的線程數(shù)量大于或等于corePoolSize,那么將這個任務(wù)放入隊列workQueue;
如果這時候隊列滿了,而且正在運行的線程數(shù)量小于 maximumPoolSize,那么還是要創(chuàng)建非核心線程立刻運行這個任務(wù);
如果隊列滿了,而且正在運行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會拋出一個拒絕執(zhí)行的異常RejectExecutionException;只有當(dāng)一個線程完成任務(wù)時,它會從隊列中取下一個任務(wù)來執(zhí)行;
而當(dāng)一個線程無事可做(也就是空閑) 且 超過一定的時間(keepAliveTime)時,線程池會判斷如果當(dāng)前運行的線程數(shù)大于corePoolSize,那么這個線程就被停掉(銷毀線程回收資源的過程),所以當(dāng)線程池的所有任務(wù)完成后,它最終會收縮到corePoolSize的大??;
4.面試官:你剛剛提到可以通過配置不同的參數(shù)創(chuàng)建出不同的線程池,那么Java中默認(rèn)實現(xiàn)好的線程池又有哪些呢?請比較它們的異同?
(1)畫外音:這其實就是考察Executors下幾種常見的線程池了,下面的回答有點官方哈!
(2)回答:
A.SingleThreadExecutor線程池:這種線程池只有一個核心線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù);如果這個唯一的線程因為異常結(jié)束,那么會有一個新的線程來替代它,此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行;其中涉及到的參數(shù)含義為:
Executors.newSingleThreadExecutor();
corePoolSize:1,只有一個核心線程在工作;
maximumPoolSize:1;
keepAliveTime:0L;
workQueue:newLinkedBlockingQueue<Runnable>(),其緩沖隊列是無界的;
B.FixedThreadPool線程池:這種線程池是固定大小的線程池,只有核心線程;每次提交一個任務(wù)就創(chuàng)建一個線程,直到線程達(dá)到線程池的最大大小;線程池的大小一旦達(dá)到最大值就會保持不變,如果某個線程因為執(zhí)行異常而結(jié)束,那么線程池會補充一個新線程;FixedThreadPool多數(shù)是針對一些很穩(wěn)定很固定的正規(guī)并發(fā)線程;
Executors.newFixedThreadPool(N); N是根據(jù)實際情況自定義設(shè)置的線程數(shù)
corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:newLinkedBlockingQueue<Runnable>(),其緩沖隊列是無界的。
C.CachedThreadPool線程池:這種線程池是無界線程池,如果線程池的大小超過了處理任務(wù)所需要的線程,那么就會回收部分空閑(60秒不執(zhí)行任務(wù))線程,當(dāng)任務(wù)數(shù)增加時,此線程池又可以智能的添加新線程來處理任務(wù);
線程池的大小完全依賴于操作系統(tǒng)(或者說JVM)能夠創(chuàng)建的最大線程大小,SynchronousQueue是一個是緩沖區(qū)為1的阻塞隊列;
緩存型池子通常用于執(zhí)行一些生存期很短的異步型任務(wù),因此在一些面向連接的daemon型Server中用得不多,但對于生存期短的異步任務(wù),它是Executor的首選;
Executors.newCachedThreadPool();
corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:newSynchronousQueue<Runnable>(), 一個緩沖區(qū)為1的阻塞隊列。
D.ScheduledThreadPool線程池:一種核心線程數(shù)固定、大小無限制的線程池;此線程池適合 定時以及周期性執(zhí)行任務(wù)需求的場景(定時任務(wù));如果閑置,非核心線程池會在DEFAULT_KEEPALIVEMILLIS時間內(nèi)回收;
Executors.newScheduledThreadPool(10);
corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:newDelayedWorkQueue()
5.面試官:如何在Java線程池中提交線程?
(1)畫外音:這個只要能回答上execute()、submit()就闊以了
(2)回答:線程池最常用的提交任務(wù)的方法有兩種:
A.execute():ExecutorService.execute方法接收一個Runable實例,它用來執(zhí)行一個任務(wù):
B.submit():ExecutorService.submit()方法返回的是Future對象;可以用isDone()來查詢Future是否已經(jīng)完成,當(dāng)任務(wù)完成時,可以通過調(diào)用get()方法來獲取結(jié)果;也可以不用isDone()進(jìn)行檢查就直接調(diào)用get(),在這種情況下,get()將阻塞,直至結(jié)果準(zhǔn)備就緒
友情提示:關(guān)于Future/FutureTask的相關(guān)知識點可以查看debug以前寫過的一遍文章:
https://www.fightjava.com/web/index/blog/article/87
6.面試官:什么是Java的內(nèi)存模型,Java中各個線程是怎么彼此看到對方的變量的?
(1)畫外音:這有點觸及知識盲區(qū)了,莫非是想聊主存和線程工作內(nèi)存……
(2)回答:Java的內(nèi)存模型定義了程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出這樣的底層細(xì)節(jié)。此處的變量包括實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但是不包括局部變量和方法參數(shù),因為這些是線程私有的,不會被共享,所以不存在競爭問題 (并發(fā)安全的源頭)
那么Java中各個線程是怎么彼此看到對方的變量的呢?:Java中定義了主內(nèi)存與工作內(nèi)存的概念:所有的變量都存儲在主內(nèi)存,每條線程還有自己的工作內(nèi)存,保存了被該線程使用到的變量的主內(nèi)存副本拷貝;
線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存的變量,不同的線程之間也無法直接訪問對方工作內(nèi)存的變量,線程間變量值的傳遞需要通過主內(nèi)存。
7.面試官: 請談?wù)?span lang="EN-US">volatile有什么特點,為什么它能保證變量對所有線程的可見性?
(1)畫外音:這問的就有點深度了~
(2)回答:關(guān)鍵字volatile是Java虛擬機提供的最輕量級的同步機制,當(dāng)一個變量被定義成volatile之后,具備兩種特性:
A.保證此變量對所有線程的可見性:當(dāng)一條線程修改了這個變量的值,新值對于其他線程是可以立即得知的,而普通變量做不到這一點。
B.禁止指令重排序優(yōu)化:普通變量僅僅能保證在該方法執(zhí)行過程中,得到正確結(jié)果,但是不保證程序代碼的執(zhí)行順序;
Java的內(nèi)存模型定義了8種內(nèi)存間操作:lock和unlock把一個變量標(biāo)識為一條線程獨占的狀態(tài),把一個處于鎖定狀態(tài)的變量釋放出來,釋放之后的變量才能被其他線程鎖定;
read和write把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存,以便load把store操作從工作內(nèi)存得到的變量的值,放入主內(nèi)存的變量中;
load和store把read操作從主內(nèi)存得到的變量值放入工作內(nèi)存的變量副本中,把工作內(nèi)存的變量值傳送到主內(nèi)存,以便write;
use和assgin把工作內(nèi)存變量值傳遞給執(zhí)行引擎,將執(zhí)行引擎值傳遞給工作內(nèi)存變量值;
volatile的實現(xiàn)基于這8種內(nèi)存間操作,保證了一個線程對某個volatile變量的修改,一定會被另一個線程看見,即保證了可見性。
(吐槽:回答得太官方了,我覺得能回答上8種內(nèi)存間的操作以及操作名就不錯了?。?span lang="EN-US">
8.面試官:既然volatile能夠保證線程間變量的可見性,是不是就意味著基于volatile變量的運算就是并發(fā)安全的?
(1)畫外音:當(dāng)然不是啦……
(2)回答:不是,基于volatile變量的運算在并發(fā)下不一定是安全的;volatile修飾的變量在各個線程的工作內(nèi)存,不存在一致性問題(各個線程的工作內(nèi)存中volatile變量,每次使用前都要刷新到主內(nèi)存);
但是Java里面的運算并非原子操作,導(dǎo)致volatile變量的運算在并發(fā)下一樣是不安全的(其實就是時間先后問題:ThreadA刷新到主內(nèi)存之前,ThreadB已經(jīng)讀取了主內(nèi)存變量最新值,導(dǎo)致不一致)
9.面試官:請簡單對比下volatile對比Synchronized的異同?
(1)畫外音:這沒辦法,只能正兒八經(jīng)的記了……
(2)回答:Synchronized既能保證可見性,又能保證原子性,而volatile只能
保證可見性,無法保證原子性;
(嘴賤多說了ThreadLocal)ThreadLocal和Synchonized都可用于解決多線程并發(fā)訪問共享資源時產(chǎn)生沖突;
但是ThreadLocal與Synchronized有本質(zhì)的區(qū)別;Synchronized用于實現(xiàn)同步機制,是利用鎖的機制使變量或代碼塊在某一時該只能被一個線程訪問,是一種“以時間換空間”的方式;而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的并不是同一個對象(除了對變量的共享),是一種“以空間換時間”的方式。
10.面試官:既然談到了ThreadLocal,那你說一說它是怎么解決并發(fā)安全的?
(1)畫外音:若能提到“線程私有內(nèi)存”、“變量副本”那基本上就闊以了
(2)回答:它是Java提供的一種保存線程私有信息的機制,因為其在整個線程生命周期內(nèi)有效,所以可以方便地在一個線程關(guān)聯(lián)的不同業(yè)務(wù)模塊之間傳遞信息,比如事務(wù)ID、Cookie等上下文相關(guān)信息。
ThreadLocal為每一個線程維護(hù)變量的副本,把共享數(shù)據(jù)的可見范圍限制在同一個線程之內(nèi),其實現(xiàn)原理是,在ThreadLocal類中有一個Map,用于存儲每一個線程的變量的副本。
11.面試官:很多人都說要慎用ThreadLocal,談?wù)勀愕睦斫猓褂?span lang="EN-US">ThreadLocal需要注意些什么?
(1)畫外音:應(yīng)該是想說remove操作吧.
(2)回答:使用ThreadLocal要注意remove;因為ThreadLocal的實現(xiàn)是其實基于一個ThreadLocalMap,在ThreadLocalMap中,它的key是一個弱引用;
而通常弱引用都會和引用隊列配合清理機制使用,但是ThreadLocal是個例外,它并沒有這么做;這意味著,廢棄項目的回收依賴于顯式地觸發(fā),否則就要等待線程結(jié)束,進(jìn)而回收相應(yīng)ThreadLocalMap,這就是很多OOM的來源;
所以通常都會建議,應(yīng)用一定要自己負(fù)責(zé)remove,并且盡量不要和線程池一起使用!
面試官:好,那咱們今天就面到這里吧?。?!
總結(jié):
我是debug,一個相信技術(shù)改變生活、技術(shù)成就夢想 的攻城獅;關(guān)注下方debug的技術(shù)公眾號,里面有更多精彩文章和技術(shù)干貨等著你哦!并動動手指收藏、點贊、以及轉(zhuǎn)發(fā)哦?。?!