新鮮出爐!面試90%會(huì)被問到的Java多線程面試題江咳,史上最全系列逢净!

前言

最近很多粉絲朋友私聊我說(shuō)能不能給整理出一份多線程面試題出來(lái),說(shuō)自己在最近的面試中老是被問到這一塊的問題被問的很煩躁歼指,前一段時(shí)間比較忙沒時(shí)間回私信爹土,前兩天看到私信我也是趕緊花了兩天給大家整理出這一塊的面試題庫(kù)出來(lái)讓大家看看,話不多說(shuō)都給大家總結(jié)在下面了踩身!

1胀茵、多線程有什么用?

一個(gè)可能在很多人看來(lái)很扯淡的一個(gè)問題:我會(huì)用多線程就好了惰赋,還管它有什么用宰掉?在我看來(lái),這個(gè)回答更扯淡赁濒。所謂知其然知其所以然轨奄,會(huì)用只是知其然,為什么用才是知其所以然拒炎,只有達(dá)到知其然知其所以然的程度才可以說(shuō)是把一個(gè)知識(shí)點(diǎn)運(yùn)用自如挪拟。OK,下面說(shuō)說(shuō)我對(duì)這個(gè)問題的看法:

(1)發(fā)揮多核CPU的優(yōu)勢(shì)

隨著工業(yè)的進(jìn)步击你,現(xiàn)在的筆記本玉组、臺(tái)式機(jī)乃至商用的應(yīng)用服務(wù)器至少也都是雙核的谎柄,4 核、8 核甚至 16 核的也都不少見惯雳,如果是單線程的程序朝巫,那么在雙核 CPU 上就浪費(fèi)了 50%,在 4 核 CPU 上就浪費(fèi)了 75%石景。單核 CPU 上所謂的多線程那是假的多線程劈猿,同一時(shí)間處理器只會(huì)處理一段邏輯,只不過線程之間切換得比較快潮孽,看著像多個(gè)線程同時(shí)運(yùn)行罷了揪荣。多核 CPU 上的多線程才是真正的多線程,它能讓你的多段邏輯同時(shí)工作往史,多線程仗颈,可以真正發(fā)揮出多核 CPU 的優(yōu)勢(shì)來(lái),達(dá)到充分利用 CPU 的目的椎例。

(2)防止阻塞

從程序運(yùn)行效率的角度來(lái)看挨决,單核 CPU 不但不會(huì)發(fā)揮出多線程的優(yōu)勢(shì),反而會(huì)因?yàn)樵趩魏?CPU 上運(yùn)行多線程導(dǎo)致線程上下文的切換订歪,而降低程序整體的效率凰棉。但是單核 CPU 我們還是要應(yīng)用多線程,就是為了防止阻塞陌粹。試想,如果單核 CPU 使用單線程福压,那么只要這個(gè)線程阻塞了掏秩,比方說(shuō)遠(yuǎn)程讀取某個(gè)數(shù)據(jù)吧,對(duì)端遲遲未返回又沒有設(shè)置超時(shí)時(shí)間荆姆,那么你的整個(gè)程序在數(shù)據(jù)返回回來(lái)之前就停止運(yùn)行了蒙幻。多線程可以防止這個(gè)問題,多條線程同時(shí)運(yùn)行胆筒,哪怕一條線程的代碼執(zhí)行讀取數(shù)據(jù)阻塞邮破,也不會(huì)影響其它任務(wù)的執(zhí)行。

(3)便于建模

這是另外一個(gè)沒有這么明顯的優(yōu)點(diǎn)了仆救。假設(shè)有一個(gè)大的任務(wù) A抒和,單線程編程,那么就要考慮很多彤蔽,建立整個(gè)程序模型比較麻煩摧莽。但是如果把這個(gè)大的任務(wù) A 分解成幾個(gè)小任務(wù),任務(wù) B顿痪、任務(wù) C镊辕、任務(wù) D油够,分別建立程序模型,并通過多線程分別運(yùn)行這幾個(gè)任務(wù)征懈,那就簡(jiǎn)單很多了石咬。

2、創(chuàng)建線程的方式

比較常見的一個(gè)問題了卖哎,一般就是兩種:

(1)繼承 Thread 類

(2)實(shí)現(xiàn) Runnable 接口

至于哪個(gè)好鬼悠,不用說(shuō)肯定是后者好,因?yàn)閷?shí)現(xiàn)接口的方式比繼承類的方式更靈活棉饶,也能減少程序之間的耦合度厦章,面向接口編程也是設(shè)計(jì)模式 6 大原則的核心。

3照藻、start() 方法和 run() 方法的區(qū)別

只有調(diào)用了 start() 方法袜啃,才會(huì)表現(xiàn)出多線程的特性,不同線程的 run() 方法里面的代碼交替執(zhí)行幸缕。如果只是調(diào)用run() 方法群发,那么代碼還是同步執(zhí)行的,必須等待一個(gè)線程的 run() 方法里面的代碼全部執(zhí)行完畢之后发乔,另外一個(gè)線程才可以執(zhí)行其run()方法里面的代碼熟妓。

4、Runnable 接口和 Callable 接口的區(qū)別

有點(diǎn)深的問題了栏尚,也看出一個(gè) Java 程序員學(xué)習(xí)知識(shí)的廣度起愈。

Runnable 接口中的 run() 方法的返回值是 void,它做的事情只是純粹地去執(zhí)行 run() 方法中的代碼而已译仗;Callable接口中的 call() 方法是有返回值的抬虽,是一個(gè)泛型,和 Future纵菌、FutureTask 配合可以用來(lái)獲取異步執(zhí)行的結(jié)果阐污。

這其實(shí)是很有用的一個(gè)特性,因?yàn)槎嗑€程相比單線程更難咱圆、更復(fù)雜的一個(gè)重要原因就是因?yàn)槎嗑€程充滿著未知性笛辟,某條線程是否執(zhí)行了?某條線程執(zhí)行了多久序苏?某條線程執(zhí)行的時(shí)候我們期望的數(shù)據(jù)是否已經(jīng)賦值完畢手幢?無(wú)法得知,我們能做的只是等待這條多線程的任務(wù)執(zhí)行完畢而已忱详。而 Callable+Future/FutureTask 卻可以獲取多線程運(yùn)行的結(jié)果弯菊,可以在等待時(shí)間太長(zhǎng)沒獲取到需要的數(shù)據(jù)的情況下取消該線程的任務(wù),真的是非常有用。

5管钳、CyclicBarrier 和 CountDownLatch 的區(qū)別

兩個(gè)看上去有點(diǎn)像的類钦铁,都在 java.util.concurrent 下,都可以用來(lái)表示代碼運(yùn)行到某個(gè)點(diǎn)上才漆,二者的區(qū)別在于:

(1)CyclicBarrier 的某個(gè)線程運(yùn)行到某個(gè)點(diǎn)上之后牛曹,該線程即停止運(yùn)行,直到所有的線程都到達(dá)了這個(gè)點(diǎn)醇滥,所有線程才重新運(yùn)行黎比;CountDownLatch 則不是,某線程運(yùn)行到某個(gè)點(diǎn)上之后鸳玩,只是給某個(gè)數(shù)值 -1 而已阅虫,該線程繼續(xù)運(yùn)行

(2)CyclicBarrier 只能喚起一個(gè)任務(wù),CountDownLatch 可以喚起多個(gè)任務(wù)

(3)CyclicBarrier 可重用不跟,CountDownLatch 不可重用颓帝,計(jì)數(shù)值為 0 該 CountDownLatch 就不可再用了

6、volatile 關(guān)鍵字的作用

一個(gè)非常重要的問題窝革,是每個(gè)學(xué)習(xí)购城、應(yīng)用多線程的 Java 程序員都必須掌握的。理解 volatile 關(guān)鍵字的作用的前提是要理解 Java 內(nèi)存模型虐译,這里就不講 Java 內(nèi)存模型了瘪板,可以參見第 31 點(diǎn),volatile 關(guān)鍵字的作用主要有兩個(gè):

(1)多線程主要圍繞可見性和原子性兩個(gè)特性而展開漆诽,使用 volatile 關(guān)鍵字修飾的變量侮攀,保證了其在多線程之間的可見性,即每次讀取到 volatile 變量厢拭,一定是最新的數(shù)據(jù)

(2)代碼底層執(zhí)行不像我們看到的高級(jí)語(yǔ)言----Java 程序這么簡(jiǎn)單魏身,它的執(zhí)行是 Java 代碼-->字節(jié)碼-->根據(jù)字節(jié)碼執(zhí)行對(duì)應(yīng)的 C/C++ 代碼--> C/C++ 代碼被編譯成匯編語(yǔ)言-->和硬件電路交互,現(xiàn)實(shí)中蚪腐,為了獲取更好的性能 JVM 可能會(huì)對(duì)指令進(jìn)行重排序,多線程下可能會(huì)出現(xiàn)一些意想不到的問題税朴。使用 volatile 則會(huì)對(duì)禁止語(yǔ)義重排序回季,當(dāng)然這也一定程度上降低了代碼執(zhí)行效率

從實(shí)踐角度而言,volatile 的一個(gè)重要作用就是和 CAS 結(jié)合正林,保證了原子性泡一,詳細(xì)的可以參見java.util.concurrent.atomic 包下的類,比如 AtomicInteger觅廓。

7鼻忠、什么是線程安全

又是一個(gè)理論的問題,各式各樣的答案有很多杈绸,我給出一個(gè)個(gè)人認(rèn)為解釋的最好的:如果你的代碼在多線程下執(zhí)行和在單線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果帖蔓,那么你的代碼就是線程安全的矮瘟。

這個(gè)問題有值得一提的地方,就是線程安全也是有幾個(gè)級(jí)別的:

(1)不可變

像 String塑娇、Integer澈侠、Long 這些,都是 final 類型的類埋酬,任何一個(gè)線程都改變不了它們的值哨啃,要改變除非新創(chuàng)建一個(gè),因此這些不可變對(duì)象不需要任何同步手段就可以直接在多線程環(huán)境下使用

(2)絕對(duì)線程安全

不管運(yùn)行時(shí)環(huán)境如何写妥,調(diào)用者都不需要額外的同步措施拳球。要做到這一點(diǎn)通常需要付出許多額外的代價(jià),Java 中標(biāo)注自己是線程安全的類珍特,實(shí)際上絕大多數(shù)都不是線程安全的祝峻,不過絕對(duì)線程安全的類,Java 中也有次坡,比方說(shuō)CopyOnWriteArrayList忧陪、CopyOnWriteArraySet

(3)相對(duì)線程安全

相對(duì)線程安全也就是我們通常意義上所說(shuō)的線程安全漱受,像 Vector 這種,add、remove 方法都是原子操作窝趣,不會(huì)被打斷,但也僅限于此织鲸,如果有個(gè)線程在遍歷某個(gè) Vector媚狰、有個(gè)線程同時(shí)在 add 這個(gè) Vector,99% 的情況下都會(huì)出現(xiàn) ConcurrentModificationException诱篷,也就是 fail-fast 機(jī)制壶唤。

(4)線程非安全

這個(gè)就沒什么好說(shuō)的了,ArrayList棕所、LinkedList闸盔、HashMap 等都是線程非安全的類

8、Java中如何獲取到線程 dump 文件

死循環(huán)琳省、死鎖迎吵、阻塞、頁(yè)面打開慢等問題针贬,打線程 dump 是最好的解決問題的途徑击费。所謂線程 dump 也就是線程堆棧,獲取到線程堆棧有兩步:

(1)獲取到線程的 pid桦他,可以通過使用 jps 命令蔫巩,在 Linux 環(huán)境下還可以使用 ps -ef | grep java

(2)打印線程堆棧,可以通過使用 jstack pid 命令,在Linux環(huán)境下還可以使用 kill -3 pid

另外提一點(diǎn)圆仔,Thread 類提供了一個(gè) getStackTrace() 方法也可以用于獲取線程堆棧垃瞧。這是一個(gè)實(shí)例方法,因此此方法是和具體線程實(shí)例綁定的荧缘,每次獲取獲取到的是具體某個(gè)線程當(dāng)前運(yùn)行的堆棧皆警,

9、一個(gè)線程如果出現(xiàn)了運(yùn)行時(shí)異常會(huì)怎么樣

如果這個(gè)異常沒有被捕獲的話截粗,這個(gè)線程就停止執(zhí)行了信姓。另外重要的一點(diǎn)是:如果這個(gè)線程持有某個(gè)某個(gè)對(duì)象的監(jiān)視器,那么這個(gè)對(duì)象監(jiān)視器會(huì)被立即釋放

10绸罗、如何在兩個(gè)線程之間共享數(shù)據(jù)

通過在線程之間共享對(duì)象就可以了意推,然后通過 wait/notify/notifyAll、await/signal/signalAll 進(jìn)行喚起和等待珊蟀,比方說(shuō)阻塞隊(duì)列 BlockingQueue 就是為線程之間共享數(shù)據(jù)而設(shè)計(jì)的

11菊值、sleep方法和wait方法有什么區(qū)別

這個(gè)問題常問,sleep 方法和 wait 方法都可以用來(lái)放棄 CPU 一定的時(shí)間育灸,不同點(diǎn)在于如果線程持有某個(gè)對(duì)象的監(jiān)視器腻窒,sleep 方法不會(huì)放棄這個(gè)對(duì)象的監(jiān)視器,wait 方法會(huì)放棄這個(gè)對(duì)象的監(jiān)視器

12磅崭、生產(chǎn)者消費(fèi)者模型的作用是什么

這個(gè)問題很理論儿子,但是很重要:

(1)通過平衡生產(chǎn)者的生產(chǎn)能力和消費(fèi)者的消費(fèi)能力來(lái)提升整個(gè)系統(tǒng)的運(yùn)行效率,這是生產(chǎn)者消費(fèi)者模型最重要的作用

(2)解耦砸喻,這是生產(chǎn)者消費(fèi)者模型附帶的作用柔逼,解耦意味著生產(chǎn)者和消費(fèi)者之間的聯(lián)系少,聯(lián)系越少越可以獨(dú)自發(fā)展而不需要收到相互的制約

13割岛、ThreadLocal有什么用

簡(jiǎn)單說(shuō) ThreadLocal 就是一種以空間換時(shí)間的做法愉适,在每個(gè) Thread 里面維護(hù)了一個(gè)以開地址法實(shí)現(xiàn)的ThreadLocal.ThreadLocalMap,把數(shù)據(jù)進(jìn)行隔離癣漆,數(shù)據(jù)不共享维咸,自然就沒有線程安全方面的問題了

14、為什么 wait() 方法和 notify()/notifyAll() 方法要在同步塊中被調(diào)用

這是 JDK 強(qiáng)制的惠爽,wait() 方法和 notify()/notifyAll() 方法在調(diào)用前都必須先獲得對(duì)象的鎖

15癌蓖、wait() 方法和 notify()/notifyAll() 方法在放棄對(duì)象監(jiān)視器時(shí)有什么區(qū)別

wait() 方法和 notify()/notifyAll() 方法在放棄對(duì)象監(jiān)視器的時(shí)候的區(qū)別在于:wait() 方法立即釋放對(duì)象監(jiān)視器,notify()/notifyAll() 方法則會(huì)等待線程剩余代碼執(zhí)行完畢才會(huì)放棄對(duì)象監(jiān)視器疆股。

16、為什么要使用線程池

避免頻繁地創(chuàng)建和銷毀線程倒槐,達(dá)到線程對(duì)象的重用旬痹。另外,使用線程池還可以根據(jù)項(xiàng)目靈活地控制并發(fā)的數(shù)目。

17两残、怎么檢測(cè)一個(gè)線程是否持有對(duì)象監(jiān)視器

我也是在網(wǎng)上看到一道多線程面試題才知道有方法可以判斷某個(gè)線程是否持有對(duì)象監(jiān)視器:Thread 類提供了一個(gè)holdsLock(Object obj) 方法永毅,當(dāng)且僅當(dāng)對(duì)象 obj 的監(jiān)視器被某條線程持有的時(shí)候才會(huì)返回 true,注意這是一個(gè)static 方法人弓,這意味著某條線程指的是當(dāng)前線程沼死。

18、synchronized 和 ReentrantLock 的區(qū)別

synchronized 是和 if崔赌、else意蛀、for、while 一樣的關(guān)鍵字健芭,ReentrantLock 是類县钥,這是二者的本質(zhì)區(qū)別。既然ReentrantLock 是類慈迈,那么它就提供了比 synchronized 更多更靈活的特性若贮,可以被繼承、可以有方法痒留、可以有各種各樣的類變量谴麦,ReentrantLock比 synchronized 的擴(kuò)展性體現(xiàn)在幾點(diǎn)上:

(1)ReentrantLock 可以對(duì)獲取鎖的等待時(shí)間進(jìn)行設(shè)置,這樣就避免了死鎖

(2)ReentrantLock 可以獲取各種鎖的信息

(3)ReentrantLock 可以靈活地實(shí)現(xiàn)多路通知

另外伸头,二者的鎖機(jī)制其實(shí)也是不一樣的匾效。ReentrantLock 底層調(diào)用的是 Unsafe 的 park 方法加鎖,synchronized操作的應(yīng)該是對(duì)象頭中mark word熊锭,這點(diǎn)我不能確定弧轧。

19、ConcurrentHashMap的并發(fā)度是什么

ConcurrentHashMap 的并發(fā)度就是 segment 的大小碗殷,默認(rèn)為 16精绎,這意味著最多同時(shí)可以有 16 條線程操作ConcurrentHashMap,這也是 ConcurrentHashMap 對(duì) Hashtable 的最大優(yōu)勢(shì)锌妻,任何情況下代乃,Hashtable 能同時(shí)有兩條線程獲取 Hashtable 中的數(shù)據(jù)嗎?

20仿粹、ReadWriteLock 是什么

首先明確一下搁吓,不是說(shuō) ReentrantLock 不好,只是 ReentrantLock 某些時(shí)候有局限吭历。如果使用 ReentrantLock堕仔,可能本身是為了防止線程 A 在寫數(shù)據(jù)、線程 B 在讀數(shù)據(jù)造成的數(shù)據(jù)不一致晌区,但這樣摩骨,如果線程 C 在讀數(shù)據(jù)通贞、線程 D 也在讀數(shù)據(jù),讀數(shù)據(jù)是不會(huì)改變數(shù)據(jù)的恼五,沒有必要加鎖昌罩,但是還是加鎖了,降低了程序的性能灾馒。

因?yàn)檫@個(gè)茎用,才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個(gè)讀寫鎖接口睬罗,ReentrantReadWriteLock 是ReadWriteLock 接口的一個(gè)具體實(shí)現(xiàn)轨功,實(shí)現(xiàn)了讀寫的分離,讀鎖是共享的傅物,寫鎖是獨(dú)占的夯辖,讀和讀之間不會(huì)互斥,讀和寫董饰、寫和讀蒿褂、寫和寫之間才會(huì)互斥,提升了讀寫的性能卒暂。

21啄栓、FutureTask 是什么

這個(gè)其實(shí)前面有提到過,F(xiàn)utureTask 表示一個(gè)異步運(yùn)算的任務(wù)也祠。FutureTask 里面可以傳入一個(gè) Callable 的具體實(shí)現(xiàn)類昙楚,可以對(duì)這個(gè)異步運(yùn)算的任務(wù)的結(jié)果進(jìn)行等待獲取、判斷是否已經(jīng)完成诈嘿、取消任務(wù)等操作堪旧。當(dāng)然,由于FutureTask 也是 Runnable 接口的實(shí)現(xiàn)類奖亚,所以 FutureTask 也可以放入線程池中淳梦。

22、Linux 環(huán)境下如何查找哪個(gè)線程使用 CPU 最長(zhǎng)

這是一個(gè)比較偏實(shí)踐的問題昔字,這種問題我覺得挺有意義的爆袍。可以這么做:

(1)獲取項(xiàng)目的 pid作郭,jps 或者 ps -ef | grep java陨囊,這個(gè)前面有講過

(2)top -H -p pid,順序不能改變

這樣就可以打印出當(dāng)前的項(xiàng)目夹攒,每條線程占用 CPU 時(shí)間的百分比蜘醋。注意這里打出的是 LWP,也就是操作系統(tǒng)原生線程的線程號(hào)咏尝,我筆記本山?jīng)]有部署 Linux 環(huán)境下的 Java 工程压语,因此沒有辦法截圖演示闲先,網(wǎng)友朋友們?nèi)绻臼鞘褂?Linux 環(huán)境部署項(xiàng)目的話,可以嘗試一下无蜂。

使用 top -H -p pid + jps pid 可以很容易地找到某條占用 CPU 高的線程的線程堆棧,從而定位占用 CPU 高的原因蒙谓,一般是因?yàn)椴划?dāng)?shù)拇a操作導(dǎo)致了死循環(huán)斥季。

最后提一點(diǎn),top -H -p pid 打出來(lái)的 LWP 是十進(jìn)制的累驮,jps pid 打出來(lái)的本地線程號(hào)是十六進(jìn)制的酣倾,轉(zhuǎn)換一下,就能定位到占用CPU高的線程的當(dāng)前線程堆棧了谤专。

23躁锡、Java 編程寫一個(gè)會(huì)導(dǎo)致死鎖的程序

第一次看到這個(gè)題目,覺得這是一個(gè)非常好的問題置侍。很多人都知道死鎖是怎么一回事兒:線程 A 和線程 B 相互等待對(duì)方持有的鎖導(dǎo)致程序無(wú)限死循環(huán)下去映之。當(dāng)然也僅限于此了,問一下怎么寫一個(gè)死鎖的程序就不知道了蜡坊,這種情況說(shuō)白了就是不懂什么是死鎖杠输,懂一個(gè)理論就完事兒了,實(shí)踐中碰到死鎖的問題基本上是看不出來(lái)的秕衙。

真正理解什么是死鎖蠢甲,這個(gè)問題其實(shí)不難,幾個(gè)步驟:

(1)兩個(gè)線程里面分別持有兩個(gè) Object 對(duì)象:lock1 和 lock2据忘。這兩個(gè) lock 作為同步代碼塊的鎖鹦牛;

(2)線程 1 的 run() 方法中同步代碼塊先獲取 lock1 的對(duì)象鎖,Thread.sleep(xxx)勇吊,時(shí)間不需要太多曼追,50 毫秒差不多了,然后接著獲取 lock2 的對(duì)象鎖萧福。這么做主要是為了防止線程 1 啟動(dòng)一下子就連續(xù)獲得了 lock1 和 lock2 兩個(gè)對(duì)象的對(duì)象鎖

(3)線程2的 run() 方法中同步代碼塊先獲取 lock2 的對(duì)象鎖拉鹃,接著獲取 lock1 的對(duì)象鎖,當(dāng)然這時(shí) lock1 的對(duì)象鎖已經(jīng)被線程 1 鎖持有鲫忍,線程 2 肯定是要等待線程 1 釋放 lock1 的對(duì)象鎖的

這樣膏燕,線程 1 睡覺睡完,線程 2 已經(jīng)獲取了 lock2 的對(duì)象鎖了悟民,線程 1 此時(shí)嘗試獲取 lock2 的對(duì)象鎖坝辫,便被阻塞,此時(shí)一個(gè)死鎖就形成了射亏。代碼就不寫了近忙,占的篇幅有點(diǎn)多竭业,Java多線程7:死鎖這篇文章里面有,就是上面步驟的代碼實(shí)現(xiàn)及舍。

24未辆、怎么喚醒一個(gè)阻塞的線程

如果線程是因?yàn)檎{(diào)用了 wait()、sleep() 或者 join() 方法而導(dǎo)致的阻塞锯玛,可以中斷線程咐柜,并且通過拋出InterruptedException 來(lái)喚醒它;如果線程遇到了 IO 阻塞攘残,無(wú)能為力拙友,因?yàn)?IO 是操作系統(tǒng)實(shí)現(xiàn)的,Java 代碼并沒有辦法直接接觸到操作系統(tǒng)歼郭。

25遗契、不可變對(duì)象對(duì)多線程有什么幫助

前面有提到過的一個(gè)問題,不可變對(duì)象保證了對(duì)象的內(nèi)存可見性病曾,對(duì)不可變對(duì)象的讀取不需要進(jìn)行額外的同步手段牍蜂,提升了代碼執(zhí)行效率。

26泰涂、什么是多線程的上下文切換

多線程的上下文切換是指 CPU 控制權(quán)由一個(gè)已經(jīng)正在運(yùn)行的線程切換到另外一個(gè)就緒并等待獲取CPU執(zhí)行權(quán)的線程的過程捷兰。

27、如果你提交任務(wù)時(shí)负敏,線程池隊(duì)列已滿贡茅,這時(shí)會(huì)發(fā)生什么

這里區(qū)分一下:

如果使用的是無(wú)界隊(duì)列 LinkedBlockingQueue,也就是無(wú)界隊(duì)列的話其做,沒關(guān)系顶考,繼續(xù)添加任務(wù)到阻塞隊(duì)列中等待執(zhí)行,因?yàn)?LinkedBlockingQueue 可以近乎認(rèn)為是一個(gè)無(wú)窮大的隊(duì)列妖泄,可以無(wú)限存放任務(wù)

如果使用的是有界隊(duì)列比如 ArrayBlockingQueue驹沿,任務(wù)首先會(huì)被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 滿了蹈胡,會(huì)根據(jù) maximumPoolSize 的值增加線程數(shù)量渊季,如果增加了線程數(shù)量還是處理不過來(lái),ArrayBlockingQueue 繼續(xù)滿罚渐,那么則會(huì)使用拒絕策略 RejectedExecutionHandler 處理滿了的任務(wù)却汉,默認(rèn)是 AbortPolicy

28、Java 中用到的線程調(diào)度算法是什么

搶占式荷并。一個(gè)線程用完 CPU 之后合砂,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行源织。

29翩伪、Thread.sleep(0) 的作用是什么

這個(gè)問題和上面那個(gè)問題是相關(guān)的微猖,我就連在一起了。由于 Java 采用搶占式的線程調(diào)度算法缘屹,因此可能會(huì)出現(xiàn)某條線程常常獲取到 CPU 控制權(quán)的情況凛剥,為了讓某些優(yōu)先級(jí)比較低的線程也能獲取到 CPU 控制權(quán),可以使用Thread.sleep(0) 手動(dòng)觸發(fā)一次操作系統(tǒng)分配時(shí)間片的操作轻姿,這也是平衡 CPU 控制權(quán)的一種操作当悔。

30、什么是自旋

很多 synchronized 里面的代碼只是一些很簡(jiǎn)單的代碼踢代,執(zhí)行時(shí)間非常快嗅骄,此時(shí)等待的線程都加鎖可能是一種不太值得的操作胳挎,因?yàn)榫€程阻塞涉及到用戶態(tài)和內(nèi)核態(tài)切換的問題。既然 synchronized 里面的代碼執(zhí)行得非衬缟快慕爬,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界做忙循環(huán)屏积,這就是自旋医窿。如果做了多次忙循環(huán)發(fā)現(xiàn)還沒有獲得鎖,再阻塞炊林,這樣可能是一種更好的策略姥卢。

31、什么是 Java 內(nèi)存模型

Java 內(nèi)存模型定義了一種多線程訪問 Java 內(nèi)存的規(guī)范渣聚。Java 內(nèi)存模型要完整講不是這里幾句話能說(shuō)清楚的独榴,我簡(jiǎn)單總結(jié)一下 Java 內(nèi)存模型的幾部分內(nèi)容:

(1)Java 內(nèi)存模型將內(nèi)存分為了主內(nèi)存和工作內(nèi)存。類的狀態(tài)奕枝,也就是類之間共享的變量棺榔,是存儲(chǔ)在主內(nèi)存中的,每次 Java 線程用到這些主內(nèi)存中的變量的時(shí)候隘道,會(huì)讀一次主內(nèi)存中的變量症歇,并讓這些內(nèi)存在自己的工作內(nèi)存中有一份拷貝,運(yùn)行自己線程代碼的時(shí)候谭梗,用到這些變量,操作的都是自己工作內(nèi)存中的那一份激捏。在線程代碼執(zhí)行完畢之后德频,會(huì)將最新的值更新到主內(nèi)存中去

(2)定義了幾個(gè)原子操作,用于操作主內(nèi)存和工作內(nèi)存中的變量

(3)定義了 volatile 變量的使用規(guī)則

(4)happens-before缩幸,即先行發(fā)生原則壹置,定義了操作 A 必然先行發(fā)生于操作 B 的一些規(guī)則竞思,比如在同一個(gè)線程內(nèi)控制流前面的代碼一定先行發(fā)生于控制流后面的代碼、一個(gè)釋放鎖 unlock 的動(dòng)作一定先行發(fā)生于后面對(duì)于同一個(gè)鎖進(jìn)行鎖定 lock 的動(dòng)作等等钞护,只要符合這些規(guī)則盖喷,則不需要額外做同步措施,如果某段代碼不符合所有的happens-before 規(guī)則难咕,則這段代碼一定是線程非安全的

32课梳、什么是 CAS

CAS,全稱為 Compare and Swap余佃,即比較-替換暮刃。假設(shè)有三個(gè)操作數(shù):內(nèi)存值 V、舊的預(yù)期值 A爆土、要修改的值B椭懊,當(dāng)且僅當(dāng)預(yù)期值 A 和內(nèi)存值 V 相同時(shí),才會(huì)將內(nèi)存值修改為 B 并返回 true步势,否則什么都不做并返回 false氧猬。當(dāng)然 CAS 一定要 volatile 變量配合,這樣才能保證每次拿到的變量是主內(nèi)存中最新的那個(gè)值坏瘩,否則舊的預(yù)期值A(chǔ)對(duì)某條線程來(lái)說(shuō)盅抚,永遠(yuǎn)是一個(gè)不會(huì)變的值A(chǔ),只要某次 CAS 操作失敗倔矾,永遠(yuǎn)都不可能成功妄均。

33、什么是樂觀鎖和悲觀鎖

(1)樂觀鎖:就像它的名字一樣哪自,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持樂觀狀態(tài)丛晦,樂觀鎖認(rèn)為競(jìng)爭(zhēng)不總是會(huì)發(fā)生,因此它不需要持有鎖提陶,將比較-替換這兩個(gè)動(dòng)作作為一個(gè)原子操作嘗試去修改內(nèi)存中的變量烫沙,如果失敗則表示發(fā)生沖突,那么就應(yīng)該有相應(yīng)的重試邏輯隙笆。

(2)悲觀鎖:還是像它的名字一樣锌蓄,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài),悲觀鎖認(rèn)為競(jìng)爭(zhēng)總是會(huì)發(fā)生撑柔,因此每次對(duì)某資源進(jìn)行操作時(shí)瘸爽,都會(huì)持有一個(gè)獨(dú)占的鎖,就像 synchronized铅忿,不管三七二十一剪决,直接上了鎖就操作資源了。

34、什么是 AQS

簡(jiǎn)單說(shuō)一下 AQS柑潦,AQS 全稱為AbstractQueuedSychronizer享言,翻譯過來(lái)應(yīng)該是抽象隊(duì)列同步器。

如果說(shuō) java.util.concurrent 的基礎(chǔ)是 CAS 的話渗鬼,那么 AQS 就是整個(gè) Java 并發(fā)包的核心了览露,ReentrantLock、CountDownLatch譬胎、Semaphore 等等都用到了它差牛。AQS 實(shí)際上以雙向隊(duì)列的形式連接所有的 Entry,比方說(shuō)ReentrantLock堰乔,所有等待的線程都被放在一個(gè) Entry 中并連成雙向隊(duì)列偏化,前面一個(gè)線程使用 ReentrantLock 好了,則雙向隊(duì)列實(shí)際上的第一個(gè)Entry開始運(yùn)行镐侯。

AQS 定義了對(duì)雙向隊(duì)列所有的操作侦讨,而只開放了 tryLock 和 tryRelease 方法給開發(fā)者使用,開發(fā)者可以根據(jù)自己的實(shí)現(xiàn)重寫 tryLock 和 tryRelease 方法析孽,以實(shí)現(xiàn)自己的并發(fā)功能。

35只怎、單例模式的線程安全性

老生常談的問題了袜瞬,首先要說(shuō)的是單例模式的線程安全意味著:某個(gè)類的實(shí)例在多線程環(huán)境下只會(huì)被創(chuàng)建一次出來(lái)。單例模式有很多種的寫法身堡,我總結(jié)一下:

(1)餓漢式單例模式的寫法:線程安全

(2)懶漢式單例模式的寫法:非線程安全

(3)雙檢鎖單例模式的寫法:線程安全

36邓尤、Semaphore 有什么作用

Semaphore 就是一個(gè)信號(hào)量,它的作用是限制某段代碼塊的并發(fā)數(shù)贴谎。Semaphore 有一個(gè)構(gòu)造函數(shù)汞扎,可以傳入一個(gè) int 型整數(shù) n,表示某段代碼最多只有 n 個(gè)線程可以訪問擅这,如果超出了 n澈魄,那么請(qǐng)等待,等到某個(gè)線程執(zhí)行完畢這段代碼塊仲翎,下一個(gè)線程再進(jìn)入痹扇。由此可以看出如果 Semaphore 構(gòu)造函數(shù)中傳入的 int 型整數(shù) n=1,相當(dāng)于變成了一個(gè) synchronized 了溯香。

37鲫构、Hashtable 的 size() 方法中明明只有一條語(yǔ)句 return count ,為什么還要做同步玫坛?

這是我之前的一個(gè)困惑结笨,不知道大家有沒有想過這個(gè)問題。某個(gè)方法中如果有多條語(yǔ)句,并且都在操作同一個(gè)類變量炕吸,那么在多線程環(huán)境下不加鎖伐憾,勢(shì)必會(huì)引發(fā)線程安全問題,這很好理解算途,但是 size() 方法明明只有一條語(yǔ)句塞耕,為什么還要加鎖?

關(guān)于這個(gè)問題嘴瓤,在慢慢地工作扫外、學(xué)習(xí)中,有了理解廓脆,主要原因有兩點(diǎn):

(1)同一時(shí)間只能有一條線程執(zhí)行固定類的同步方法筛谚,但是對(duì)于類的非同步方法,可以多條線程同時(shí)訪問停忿。所以驾讲,這樣就有問題了,可能線程 A 在執(zhí)行 Hashtable 的 put 方法添加數(shù)據(jù)席赂,線程 B 則可以正常調(diào)用 size() 方法讀取 Hashtable 中當(dāng)前元素的個(gè)數(shù)吮铭,那讀取到的值可能不是最新的,可能線程 A 添加了完了數(shù)據(jù)颅停,但是沒有對(duì)size++谓晌,線程 B 就已經(jīng)讀取 size 了,那么對(duì)于線程 B 來(lái)說(shuō)讀取到的 size 一定是不準(zhǔn)確的癞揉。而給 size() 方法加了同步之后纸肉,意味著線程 B 調(diào)用size () 方法只有在線程 A 調(diào)用 put 方法完畢之后才可以調(diào)用,這樣就保證了線程安全性

(2)CPU 執(zhí)行代碼喊熟,執(zhí)行的不是 Java 代碼柏肪,這點(diǎn)很關(guān)鍵,一定得記住芥牌。Java 代碼最終是被翻譯成機(jī)器碼執(zhí)行的烦味,機(jī)器碼才是真正可以和硬件電路交互的代碼。即使你看到 Java 代碼只有一行壁拉,甚至你看到 Java 代碼編譯之后生成的字節(jié)碼也只有一行拐叉,也不意味著對(duì)于底層來(lái)說(shuō)這句語(yǔ)句的操作只有一個(gè)。一句 return count 假設(shè)被翻譯成了三句匯編語(yǔ)句執(zhí)行扇商,一句匯編語(yǔ)句和其機(jī)器碼做對(duì)應(yīng)凤瘦,完全可能執(zhí)行完第一句,線程就切換了案铺。

38蔬芥、線程類的構(gòu)造方法、靜態(tài)塊是被哪個(gè)線程調(diào)用的

這是一個(gè)非常刁鉆和狡猾的問題。請(qǐng)記妆仕小:線程類的構(gòu)造方法返吻、靜態(tài)塊是被 new 這個(gè)線程類所在的線程所調(diào)用的,而 run 方法里面的代碼才是被線程自身所調(diào)用的乎婿。

如果說(shuō)上面的說(shuō)法讓你感到困惑测僵,那么我舉個(gè)例子,假設(shè) Thread2 中 new 了 Thread1谢翎,main 函數(shù)中 new 了Thread2捍靠,那么:

(1)Thread2 的構(gòu)造方法、靜態(tài)塊是 main 線程調(diào)用的森逮,Thread2 的 run() 方法是 Thread2 自己調(diào)用的

(2)Thread1 的構(gòu)造方法榨婆、靜態(tài)塊是 Thread2 調(diào)用的,Thread1 的 run() 方法是 Thread1 自己調(diào)用的

39褒侧、同步方法和同步塊良风,哪個(gè)是更好的選擇

同步塊,這意味著同步塊之外的代碼是異步執(zhí)行的闷供,這比同步整個(gè)方法更提升代碼的效率烟央。請(qǐng)知道一條原則:同步的范圍越小越好。

借著這一條歪脏,我額外提一點(diǎn)疑俭,雖說(shuō)同步的范圍越少越好,但是在 Java 虛擬機(jī)中還是存在著一種叫做鎖粗化的優(yōu)化方法唾糯,這種方法就是把同步范圍變大怠硼。這是有用的鬼贱,比方說(shuō) StringBuffer移怯,它是一個(gè)線程安全的類,自然最常用的append() 方法是一個(gè)同步方法这难,我們寫代碼的時(shí)候會(huì)反復(fù) append 字符串舟误,這意味著要進(jìn)行反復(fù)的加鎖->解鎖,這對(duì)性能不利姻乓,因?yàn)檫@意味著 Java 虛擬機(jī)在這條線程上要反復(fù)地在內(nèi)核態(tài)和用戶態(tài)之間進(jìn)行切換嵌溢,因此 Java 虛擬機(jī)會(huì)將多次 append 方法調(diào)用的代碼進(jìn)行一個(gè)鎖粗化的操作,將多次的 append 的操作擴(kuò)展到 append 方法的頭尾蹋岩,變成一個(gè)大的同步塊赖草,這樣就減少了加鎖-->解鎖的次數(shù),有效地提升了代碼執(zhí)行的效率剪个。

40秧骑、高并發(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ǎng)上看到的一個(gè)問題吓蘑,把這個(gè)問題放在最后一個(gè),希望每個(gè)人都能看到并且思考一下坟冲,因?yàn)檫@個(gè)問題非常好磨镶、非常實(shí)際、非常專業(yè)樱衷。關(guān)于這個(gè)問題棋嘲,個(gè)人看法是:

(1)高并發(fā)、任務(wù)執(zhí)行時(shí)間短的業(yè)務(wù)矩桂,線程池線程數(shù)可以設(shè)置為 CPU 核數(shù)+1沸移,減少線程上下文的切換

(2)并發(fā)不高、任務(wù)執(zhí)行時(shí)間長(zhǎng)的業(yè)務(wù)要區(qū)分開看:

a)假如是業(yè)務(wù)時(shí)間長(zhǎng)集中在 IO 操作上侄榴,也就是 IO 密集型的任務(wù)雹锣,因?yàn)?IO 操作并不占用 CPU,所以不要讓所有的 CPU 閑下來(lái)癞蚕,可以加大線程池中的線程數(shù)目蕊爵,讓 CPU 處理更多的業(yè)務(wù)

b)假如是業(yè)務(wù)時(shí)間長(zhǎng)集中在計(jì)算操作上,也就是計(jì)算密集型任務(wù)桦山,這個(gè)就沒辦法了攒射,和(1)一樣吧,線程池中的線程數(shù)設(shè)置得少一些恒水,減少線程上下文的切換

(3)并發(fā)高会放、業(yè)務(wù)執(zhí)行時(shí)間長(zhǎng),解決這種類型任務(wù)的關(guān)鍵不在于線程池而在于整體架構(gòu)的設(shè)計(jì)钉凌,看看這些業(yè)務(wù)里面某些數(shù)據(jù)是否能做緩存是第一步咧最,增加服務(wù)器是第二步,至于線程池的設(shè)置御雕,設(shè)置參考(2)矢沿。最后,業(yè)務(wù)執(zhí)行時(shí)間長(zhǎng)的問題酸纲,也可能需要分析一下捣鲸,看看能不能使用中間件對(duì)任務(wù)進(jìn)行拆分和解耦。

總結(jié):今天的多線程面試題就到這里了闽坡,希望你們看完都有收獲栽惶,在自己的面試中都能拿下自己的offer脓诡!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市媒役,隨后出現(xiàn)的幾起案子祝谚,更是在濱河造成了極大的恐慌,老刑警劉巖酣衷,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件交惯,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡穿仪,警方通過查閱死者的電腦和手機(jī)席爽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)啊片,“玉大人只锻,你說(shuō)我怎么就攤上這事∽瞎龋” “怎么了齐饮?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)笤昨。 經(jīng)常有香客問我祖驱,道長(zhǎng),這世上最難降的妖魔是什么瞒窒? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任捺僻,我火速辦了婚禮,結(jié)果婚禮上崇裁,老公的妹妹穿的比我還像新娘匕坯。我一直安慰自己,他們只是感情好拔稳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布葛峻。 她就那樣靜靜地躺著,像睡著了一般壳炎。 火紅的嫁衣襯著肌膚如雪泞歉。 梳的紋絲不亂的頭發(fā)上逼侦,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天匿辩,我揣著相機(jī)與錄音,去河邊找鬼榛丢。 笑死铲球,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的晰赞。 我是一名探鬼主播稼病,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼选侨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了然走?” 一聲冷哼從身側(cè)響起援制,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芍瑞,沒想到半個(gè)月后晨仑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拆檬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年洪己,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竟贯。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡答捕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屑那,到底是詐尸還是另有隱情拱镐,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布持际,位于F島的核電站痢站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏选酗。R本人自食惡果不足惜阵难,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芒填。 院中可真熱鬧呜叫,春花似錦、人聲如沸殿衰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)闷祥。三九已至娱颊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凯砍,已是汗流浹背箱硕。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留悟衩,地道東北人剧罩。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像座泳,于是被迫代替她去往敵國(guó)和親惠昔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子幕与,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353