Java面試系列之并發(fā)編程專題-Java線程池靈魂拷問(wèn)

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。


金三銀四跳槽季即將來(lái)臨,想必有些猿友已經(jīng)蠢蠢欲動(dòng)在做相關(guān)的準(zhǔn)備了!在接下來(lái)的日子里,筆者將堅(jiān)持寫作、分享Java工程師在面試求職期間的方方面面,包括簡(jiǎn)歷制作、面試場(chǎng)景復(fù)現(xiàn)、面試題解答、談薪技巧 以及 項(xiàng)目的實(shí)戰(zhàn)!今天我們來(lái)聊聊Java中線程池相關(guān)的知識(shí)!


在上一篇文章中我們模擬了一個(gè)面試場(chǎng)景,靈魂式拷問(wèn)了JavaSynchronized相關(guān)的知識(shí),可以點(diǎn)擊鏈接查看詳情:Java面試系列之并發(fā)編程專題-Synchronized靈魂拷問(wèn)

 

下面我們開(kāi)擼!值得一提的是,以下內(nèi)容來(lái)自程序員實(shí)戰(zhàn)基地fightjava.com一位網(wǎng)友最近的面試場(chǎng)景,筆者嘗試著將其復(fù)現(xiàn),其中有些是筆者自行整理補(bǔ)充的,若有不對(duì)的地方還請(qǐng)多多指正!!

 

1. 面試官:Java線程池底層是怎么實(shí)現(xiàn)的?大概說(shuō)下

1)畫外音TND的,一上來(lái)就問(wèn)底層實(shí)現(xiàn)原理……真應(yīng)了那句“面試造火箭,工作擰螺絲”,沒(méi)辦法只能硬著頭皮上,既然說(shuō)“大概說(shuō)下”,那大概能回答 “工作線程隊(duì)列”和“任務(wù)隊(duì)列”應(yīng)該就闊以了!


2回答:在Java中,所謂的線程池中的“線程”,其實(shí)是被抽象為一個(gè)靜態(tài)內(nèi)部類Worker,即“工作線程”,它基于AQS(抽象隊(duì)列同步器)實(shí)現(xiàn)、存放在線程池一個(gè)成員變量中,其名為:“工作線程隊(duì)列” HashSet<Worker> workers,而將等待被執(zhí)行的任務(wù)存放在成員變量 “任務(wù)隊(duì)列” workQueueBlockingQueue<Runnable> workQueue)中;

這樣一來(lái),整個(gè)線程池實(shí)現(xiàn)的基本思想大概就是:從任務(wù)隊(duì)列workQueue中不斷取出需要執(zhí)行的任務(wù),放在工作線程隊(duì)列Workers中進(jìn)行處理;

 

2.面試官:嗯,不錯(cuò),說(shuō)一說(shuō)創(chuàng)建線程池的幾個(gè)核心構(gòu)造參數(shù)?

1)畫外音:這個(gè)倒不難,擼過(guò)ThreadPoolExecutor的估計(jì)都曉得!

 

2回答Java中創(chuàng)建線程池其實(shí)非常靈活,我們可以通過(guò)配置不同的參數(shù),創(chuàng)建出行為不同的線程池,這幾個(gè)參數(shù)包括:

A. corePoolSize:線程池的核心線程數(shù);

B. maximumPoolSize:線程池允許的最大線程數(shù);

C. keepAliveTime:超過(guò)核心線程數(shù)時(shí)閑置線程的存活時(shí)間;

D. workQueue:任務(wù)執(zhí)行前保存任務(wù)的隊(duì)列,保存著execute方法待提交的Runnable任務(wù);

附上代碼:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);

 

3.面試官:那線程池中的線程是怎么創(chuàng)建的?是一開(kāi)始就隨著線程池的啟動(dòng)就創(chuàng)建好的嗎?

1)畫外音:這還是挺考驗(yàn)底層源碼閱讀能力的,看過(guò)ThreadPoolExecutor的創(chuàng)建、executor下的執(zhí)行方法API execute()方法的應(yīng)該可以回答上!

 

2回答不是;線程池在創(chuàng)建后執(zhí)行初始化策略時(shí)默認(rèn)是不啟動(dòng)工作線程Worker的,而是等待有請(qǐng)求到來(lái)時(shí)才啟動(dòng),每當(dāng)我們調(diào)用execute()方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判斷:


A.如果正在運(yùn)行的線程數(shù)量小于corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);如果正在運(yùn)行的線程數(shù)量大于或等于corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列workQueue;


如果這時(shí)候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于 maximumPoolSize,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行這個(gè)任務(wù);

如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會(huì)拋出一個(gè)拒絕執(zhí)行的異常RejectExecutionException;只有當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來(lái)執(zhí)行;

而當(dāng)一個(gè)線程無(wú)事可做(也就是空閑) 且 超過(guò)一定的時(shí)間(keepAliveTime)時(shí),線程池會(huì)判斷如果當(dāng)前運(yùn)行的線程數(shù)大于corePoolSize,那么這個(gè)線程就被停掉(銷毀線程回收資源的過(guò)程),所以當(dāng)線程池的所有任務(wù)完成后,它最終會(huì)收縮到corePoolSize的大?。?span lang="EN-US">


4.面試官:你剛剛提到可以通過(guò)配置不同的參數(shù)創(chuàng)建出不同的線程池,那么Java中默認(rèn)實(shí)現(xiàn)好的線程池又有哪些呢?請(qǐng)比較它們的異同?

1)畫外音:這其實(shí)就是考察Executors下幾種常見(jiàn)的線程池了,下面的回答有點(diǎn)官方哈!

 

2回答

A.SingleThreadExecutor線程池:這種線程池只有一個(gè)核心線程在工作,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù);如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束,那么會(huì)有一個(gè)新的線程來(lái)替代它,此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行;其中涉及到的參數(shù)含義為:

Executors.newSingleThreadExecutor();

corePoolSize:1,只有一個(gè)核心線程在工作;
maximumPoolSize:1;
keepAliveTime:0L;
workQueue:newLinkedBlockingQueue<Runnable>(),其緩沖隊(duì)列是無(wú)界的;


B.FixedThreadPool線程池:這種線程池是固定大小的線程池,只有核心線程;每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程,直到線程達(dá)到線程池的最大大??;線程池的大小一旦達(dá)到最大值就會(huì)保持不變,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束,那么線程池會(huì)補(bǔ)充一個(gè)新線程;FixedThreadPool多數(shù)是針對(duì)一些很穩(wěn)定很固定的正規(guī)并發(fā)線程;

Executors.newFixedThreadPool(N);  N是根據(jù)實(shí)際情況自定義設(shè)置的線程數(shù)

corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:newLinkedBlockingQueue<Runnable>(),其緩沖隊(duì)列是無(wú)界的。


C.CachedThreadPool線程池:這種線程池是無(wú)界線程池,如果線程池的大小超過(guò)了處理任務(wù)所需要的線程,那么就會(huì)回收部分空閑(60秒不執(zhí)行任務(wù))線程,當(dāng)任務(wù)數(shù)增加時(shí),此線程池又可以智能的添加新線程來(lái)處理任務(wù);

線程池的大小完全依賴于操作系統(tǒng)(或者說(shuō)JVM)能夠創(chuàng)建的最大線程大小,SynchronousQueue是一個(gè)是緩沖區(qū)為1的阻塞隊(duì)列;

緩存型池子通常用于執(zhí)行一些生存期很短的異步型任務(wù),因此在一些面向連接的daemonServer中用得不多,但對(duì)于生存期短的異步任務(wù),它是Executor的首選;

Executors.newCachedThreadPool();

corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:newSynchronousQueue<Runnable>(), 一個(gè)緩沖區(qū)為1的阻塞隊(duì)列。


D.ScheduledThreadPool線程池:一種核心線程數(shù)固定、大小無(wú)限制的線程池;此線程池適合 定時(shí)以及周期性執(zhí)行任務(wù)需求的場(chǎng)景(定時(shí)任務(wù));如果閑置,非核心線程池會(huì)在DEFAULT_KEEPALIVEMILLIS時(shí)間內(nèi)回收;

Executors.newScheduledThreadPool(10);

corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:newDelayedWorkQueue()


5.面試官:如何在Java線程池中提交線程?

1)畫外音:這個(gè)只要能回答上execute()、submit()就闊以了

 

2回答:線程池最常用的提交任務(wù)的方法有兩種:

A.execute()ExecutorService.execute方法接收一個(gè)Runable實(shí)例,它用來(lái)執(zhí)行一個(gè)任務(wù):

B.submit()ExecutorService.submit()方法返回的是Future對(duì)象;可以用isDone()來(lái)查詢Future是否已經(jīng)完成,當(dāng)任務(wù)完成時(shí),可以通過(guò)調(diào)用get()方法來(lái)獲取結(jié)果;也可以不用isDone()進(jìn)行檢查就直接調(diào)用get(),在這種情況下,get()將阻塞,直至結(jié)果準(zhǔn)備就緒


友情提示:關(guān)于Future/FutureTask的相關(guān)知識(shí)點(diǎn)可以查看debug以前寫過(guò)的一遍文章:

https://www.fightjava.com/web/index/blog/article/87


6.面試官:什么是Java的內(nèi)存模型,Java中各個(gè)線程是怎么彼此看到對(duì)方的變量的?

1)畫外音:這有點(diǎn)觸及知識(shí)盲區(qū)了,莫非是想聊主存和線程工作內(nèi)存……

 

2回答Java的內(nèi)存模型定義了程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出這樣的底層細(xì)節(jié)。此處的變量包括實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量和方法參數(shù),因?yàn)檫@些是線程私有的,不會(huì)被共享,所以不存在競(jìng)爭(zhēng)問(wèn)題 (并發(fā)安全的源頭)

那么Java中各個(gè)線程是怎么彼此看到對(duì)方的變量的呢?:Java中定義了主內(nèi)存與工作內(nèi)存的概念:所有的變量都存儲(chǔ)在主內(nèi)存,每條線程還有自己的工作內(nèi)存,保存了被該線程使用到的變量的主內(nèi)存副本拷貝;

線程對(duì)變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存的變量,不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存的變量,線程間變量值的傳遞需要通過(guò)主內(nèi)存。

 

7.面試官: 請(qǐng)談?wù)?span lang="EN-US">volatile有什么特點(diǎn),為什么它能保證變量對(duì)所有線程的可見(jiàn)性?

1)畫外音:這問(wèn)的就有點(diǎn)深度了~

 

2回答:關(guān)鍵字volatileJava虛擬機(jī)提供的最輕量級(jí)的同步機(jī)制,當(dāng)一個(gè)變量被定義成volatile之后,具備兩種特性:

A.保證此變量對(duì)所有線程的可見(jiàn)性:當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程是可以立即得知的,而普通變量做不到這一點(diǎn)。

B.禁止指令重排序優(yōu)化:普通變量?jī)H僅能保證在該方法執(zhí)行過(guò)程中,得到正確結(jié)果,但是不保證程序代碼的執(zhí)行順序;

Java的內(nèi)存模型定義了8種內(nèi)存間操作:lockunlock把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài),把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放之后的變量才能被其他線程鎖定;

readwrite把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存,以便loadstore操作從工作內(nèi)存得到的變量的值,放入主內(nèi)存的變量中;

loadstoreread操作從主內(nèi)存得到的變量值放入工作內(nèi)存的變量副本中,把工作內(nèi)存的變量值傳送到主內(nèi)存,以便write

useassgin把工作內(nèi)存變量值傳遞給執(zhí)行引擎,將執(zhí)行引擎值傳遞給工作內(nèi)存變量值;

volatile的實(shí)現(xiàn)基于這8種內(nèi)存間操作,保證了一個(gè)線程對(duì)某個(gè)volatile變量的修改,一定會(huì)被另一個(gè)線程看見(jiàn),即保證了可見(jiàn)性。

吐槽:回答得太官方了,我覺(jué)得能回答上8種內(nèi)存間的操作以及操作名就不錯(cuò)了?。?span lang="EN-US">

 

8.面試官:既然volatile能夠保證線程間變量的可見(jiàn)性,是不是就意味著基于volatile變量的運(yùn)算就是并發(fā)安全的?

1)畫外音:當(dāng)然不是啦……

2回答不是,基于volatile變量的運(yùn)算在并發(fā)下不一定是安全的;volatile修飾的變量在各個(gè)線程的工作內(nèi)存,不存在一致性問(wèn)題(各個(gè)線程的工作內(nèi)存中volatile變量,每次使用前都要刷新到主內(nèi)存);

但是Java里面的運(yùn)算并非原子操作,導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的(其實(shí)就是時(shí)間先后問(wèn)題:ThreadA刷新到主內(nèi)存之前,ThreadB已經(jīng)讀取了主內(nèi)存變量最新值,導(dǎo)致不一致)

 

9.面試官:請(qǐng)簡(jiǎn)單對(duì)比下volatile對(duì)比Synchronized的異同?

1)畫外音:這沒(méi)辦法,只能正兒八經(jīng)的記了……

2回答Synchronized既能保證可見(jiàn)性,又能保證原子性,而volatile只能

保證可見(jiàn)性,無(wú)法保證原子性;

嘴賤多說(shuō)了ThreadLocalThreadLocalSynchonized都可用于解決多線程并發(fā)訪問(wèn)共享資源時(shí)產(chǎn)生沖突;

但是ThreadLocalSynchronized有本質(zhì)的區(qū)別;Synchronized用于實(shí)現(xiàn)同步機(jī)制,是利用鎖的機(jī)制使變量或代碼塊在某一時(shí)該只能被一個(gè)線程訪問(wèn),是一種“以時(shí)間換空間”的方式;而ThreadLocal為每一個(gè)線程都提供了變量的副本,使得每個(gè)線程在某一時(shí)間訪問(wèn)到的并不是同一個(gè)對(duì)象(除了對(duì)變量的共享),是一種“以空間換時(shí)間”的方式。


10.面試官:既然談到了ThreadLocal,那你說(shuō)一說(shuō)它是怎么解決并發(fā)安全的?

1)畫外音:若能提到“線程私有內(nèi)存”、“變量副本”那基本上就闊以了

 

2回答:它是Java提供的一種保存線程私有信息的機(jī)制,因?yàn)槠湓谡麄€(gè)線程生命周期內(nèi)有效,所以可以方便地在一個(gè)線程關(guān)聯(lián)的不同業(yè)務(wù)模塊之間傳遞信息,比如事務(wù)ID、Cookie等上下文相關(guān)信息。

ThreadLocal為每一個(gè)線程維護(hù)變量的副本,把共享數(shù)據(jù)的可見(jiàn)范圍限制在同一個(gè)線程之內(nèi),其實(shí)現(xiàn)原理是,在ThreadLocal類中有一個(gè)Map,用于存儲(chǔ)每一個(gè)線程的變量的副本。


11.面試官:很多人都說(shuō)要慎用ThreadLocal,談?wù)勀愕睦斫?,使?span lang="EN-US">ThreadLocal需要注意些什么?

1)畫外音:應(yīng)該是想說(shuō)remove操作吧.

 

2回答:使用ThreadLocal要注意remove;因?yàn)?span lang="EN-US">ThreadLocal的實(shí)現(xiàn)是其實(shí)基于一個(gè)ThreadLocalMap,在ThreadLocalMap中,它的key是一個(gè)弱引用;

而通常弱引用都會(huì)和引用隊(duì)列配合清理機(jī)制使用,但是ThreadLocal是個(gè)例外,它并沒(méi)有這么做;這意味著,廢棄項(xiàng)目的回收依賴于顯式地觸發(fā),否則就要等待線程結(jié)束,進(jìn)而回收相應(yīng)ThreadLocalMap,這就是很多OOM的來(lái)源;

所以通常都會(huì)建議,應(yīng)用一定要自己負(fù)責(zé)remove,并且盡量不要和線程池一起使用!


面試官:好,那咱們今天就面到這里吧!??!


總結(jié):

我是debug,一個(gè)相信技術(shù)改變生活、技術(shù)成就夢(mèng)想 的攻城獅;關(guān)注下方debug的技術(shù)公眾號(hào),里面有更多精彩文章和技術(shù)干貨等著你哦!并動(dòng)動(dòng)手指收藏、點(diǎn)贊、以及轉(zhuǎn)發(fā)哦?。?!