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)的知識!


在上一篇文章中我們模擬了一個面試場景,靈魂式拷問了JavaSynchronized相關(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ù)隊列” workQueueBlockingQueue<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ù),因此在一些面向連接的daemonServer中用得不多,但對于生存期短的異步任務(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)鍵字volatileJava虛擬機提供的最輕量級的同步機制,當(dāng)一個變量被定義成volatile之后,具備兩種特性:

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

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

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

readwrite把一個變量值從主內(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的實現(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只能

保證可見性,無法保證原子性;

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

但是ThreadLocalSynchronized有本質(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ā)哦?。?!