說明:答案要變成自己的話去描述,不要死記硬背。
● 在實(shí)際開發(fā)中用過多線程嗎,具體怎么用的,解決什么問題?
我們通常在后臺執(zhí)行定時任務(wù)的時候會用到多線程,例如我們在P2P項(xiàng)目中給用戶回款的時候,數(shù)據(jù)量比較大,收益需要在指定的時間之前全部返還,不然客戶這邊投訴的電話就來了,當(dāng)時我們使用了JUC包下的Excutor線程池,啟動多線程跑數(shù)據(jù)。這樣問題就解決了。
● 你對java的內(nèi)存模型有了解嗎?
面試官問這個問題的時候,同學(xué)們要注意了,面試官要聽的不是“JVM的內(nèi)存結(jié)構(gòu)”,而是Java的內(nèi)存模型,簡稱JMM(Java Memory Model),參見另一個文件:Java內(nèi)存模型JMM.docx
● 線程之間怎么通信?
另請參見:Java線程間的通信.docx
● 什么是線程安全,怎么理解的?
如果你的代碼在多線程下執(zhí)行和在單線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果,那么你的代碼就是線程安全的。
導(dǎo)致線程不安全的原因是:多線程同時對某個共享的數(shù)據(jù)進(jìn)行修改操作,此時就會存在數(shù)據(jù)不一致問題。在Java中線程安全也是有幾個級別的:
不可變
像String,其中String字符串在JVM中有字符串常量池的存在,字符串對象一旦創(chuàng)建不可變,由于這些對象在多線程并發(fā)的情況下不會被修改,所以不存在線程安全問題。
絕對線程安全
不管運(yùn)行時環(huán)境如何,調(diào)用者都不需要額外的同步措施。要做到這一點(diǎn)通常需要付出許多額外的代價,Java中標(biāo)注自己是線程安全的類,實(shí)際上絕大多數(shù)都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet。
相對線程安全
相對線程安全也就是我們通常意義上所說的線程安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限于此,如果有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的情況下都會出現(xiàn)ConcurrentModificationException,也就是 fail-fast機(jī)制。
非線程安全
這個就沒什么好說的了,ArrayList、LinkedList、HashMap等都是線程非安全的類。
● 線程之間如何共享數(shù)據(jù)?
(1)多個線程對共享數(shù)據(jù)的操作是相同的,那么創(chuàng)建一個Runnable的子類對象,將這個對象作為參數(shù)傳遞給Thread的構(gòu)造方法,此時因?yàn)槎鄠€線程操作的是同一個Runnable的子類對象,所以他們操作的是同一個共享數(shù)據(jù)。比如:買票系統(tǒng),所以的線程的操作都是對票數(shù)減一的操作。
(2)多個線程對共享數(shù)據(jù)的操作是不同的,將共享數(shù)據(jù)和操作共享數(shù)據(jù)的方法放在同一對象中,將這個對象作為參數(shù)傳遞給Runnable的子類,在子類中用該對象的方法對共享數(shù)據(jù)進(jìn)行操作。比如實(shí)現(xiàn)生產(chǎn)者和消費(fèi)者模型。
(3)多個線程對共享數(shù)據(jù)的操作是不同的, 用內(nèi)部類的方式去實(shí)現(xiàn),創(chuàng)建Runnable的子類作為內(nèi)部類,將共享對象作為全局變量,在Runnable的子類中對共享數(shù)據(jù)進(jìn)行操作。
● 線程的start()和run()方法的區(qū)別?
start()方法表示啟動一個新的線程,在JVM內(nèi)存中會開啟一個新的棧空間。而run()方法只是普通方法調(diào)用,不會啟動新的線程。
● 實(shí)現(xiàn)線程的方式分別是什么?
第一種:繼承Thread
第二種:實(shí)現(xiàn)Runnable接口,這種方式使用較多,面向接口編程一直是被推崇的開發(fā)原則。
第三種:實(shí)現(xiàn)Callable接口用來實(shí)現(xiàn)返回結(jié)果的線程
● 怎么獲取線程的返回值?
使用ExecutorService、Callable、Future實(shí)現(xiàn)有返回結(jié)果的線程。可返回值的任務(wù)必須實(shí)現(xiàn)Callable接口。執(zhí)行Callable任務(wù)后,可以獲取一個Future的對象,在該對象上調(diào)用get就可以獲取到Callable任務(wù)返回的Object了。get方法是阻塞的,即:線程無返回結(jié)果,get方法會一直等待。再結(jié)合線程池接口ExecutorService就可以實(shí)現(xiàn)傳說中有返回結(jié)果的多線程了。
● volatile關(guān)鍵字的作用是什么?
參見《java并發(fā)編程之volatile關(guān)鍵字解析.docx》
● CyclicBarrier(籬柵)和CountDownLatch(倒計(jì)時鎖存器)的區(qū)別是什么?
都在java.util.concurrent下,都可以用來表示代碼運(yùn)行到某個點(diǎn)上,二者的區(qū)別在于:
(1)CyclicBarrier的某個線程運(yùn)行到某個點(diǎn)上之后,該線程即停止運(yùn)行,直到所有的線程都到達(dá)了這個點(diǎn),所有線程才重新運(yùn)行;
CountDownLatch則不是,某線程運(yùn)行到某個點(diǎn)上之后,只是給某個數(shù)值-1而已,該線程繼續(xù)運(yùn)行。
(2)CyclicBarrier只能喚起一個任務(wù),CountDownLatch可以喚起多個任務(wù)。
(3)CyclicBarrier可重用,CountDownLatch不可重用,計(jì)數(shù)值為0該CountDownLatch就不可再用了。
● volatile(不穩(wěn)定的)和synchronized(同步的)對比?
讀以下內(nèi)容之前,參見:《java并發(fā)編程之volatile關(guān)鍵字解析.docx》
一旦一個共享變量(類的成員變量、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:
第一:保證了不同線程對這個變量進(jìn)行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
第二:禁止進(jìn)行指令重排序。
volatile本質(zhì)是在告訴jvm當(dāng)前變量在寄存器(工作內(nèi)存)中的值是不確定的,需要從主存中讀取;
(1)synchronized則是鎖定當(dāng)前變量,只有當(dāng)前線程可以訪問該變量,其他線程被阻塞住。
(2)volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的。
(3)volatile僅能實(shí)現(xiàn)變量的修改可見性,并不能保證原子性;synchronized則可以保證變量的修改可見性和原子性。
(4)volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
(5)volatile標(biāo)記的變量不會被編譯器優(yōu)化;synchronized標(biāo)記的變量可以被編譯器優(yōu)化。
● 怎么喚醒一個阻塞的線程?
如果線程是因?yàn)檎{(diào)用了wait()、sleep()或者join()方法而導(dǎo)致的阻塞,可以中斷線程,并且通過拋出InterruptedException來喚醒它;如果線程遇到了IO阻塞,無能為力,因?yàn)镮O是操作系統(tǒng)實(shí)現(xiàn)的,Java代碼并沒有辦法直接接觸到操作系統(tǒng)。
● Java中如何獲取到線程dump文件?
當(dāng)程序遇到死循環(huán)、死鎖、阻塞、頁面打開慢等問題,查看dump信息是最好的解決問題的途徑。線程dump也就是線程堆棧信息。
獲取到線程堆棧dump文件內(nèi)容分兩步:
(1)第一步:獲取到線程的pid,Linux環(huán)境下可以使用ps -ef | grep java
(2)第二步:打印線程堆棧,可以通過使用jstack pid命令
具體實(shí)現(xiàn)步驟,參照:https://blog.csdn.net/u010271462/article/details/70171553
● sleep方法和wait方法的相同點(diǎn)和不同點(diǎn)?
相同點(diǎn):
二者都可以讓線程處于凍結(jié)狀態(tài)。
不同點(diǎn):
(1)首先應(yīng)該明確sleep方法是Thread類中定義的方法,而wait方法是Object類中定義的方法。
(2)sleep方法必須人為地為其指定時間。wait方法既可以指定時間,也可以不指定時間。
(3)sleep方法時間到,線程處于臨時阻塞狀態(tài)或者運(yùn)行狀態(tài)。wait方法如果沒有被設(shè)置時間,就必須要通過notify或者notifyAll來喚醒。
(4)sleep方法不一定非要定義在同步中。wait方法必須定義在同步中。
(5)當(dāng)二者都定義在同步中時,線程執(zhí)行到sleep,不會釋放鎖。線程執(zhí)行到wait,會釋放鎖。
● 生產(chǎn)者和消費(fèi)者模型的作用是什么?
(1)通過平衡生產(chǎn)者的生產(chǎn)能力和消費(fèi)者的消費(fèi)能力來提升整個系統(tǒng)的運(yùn)行效率,這是生產(chǎn)者消費(fèi)者模型最重要的作用
(2)解耦,這是生產(chǎn)者消費(fèi)者模型附帶的作用,解耦意味著生產(chǎn)者和消費(fèi)者之間的聯(lián)系少,聯(lián)系越少越可以獨(dú)自發(fā)展而不需要受到相互的制約。
● ThreadLocal有什么作用?
ThreadLocal用來解決多線程程序的并發(fā)問題。將數(shù)據(jù)放到ThreadLocal當(dāng)中可以將該數(shù)據(jù)和當(dāng)前線程綁定在一起,這樣可以保證一個線程對應(yīng)一個對象,這樣對象就是線程安全的。
● wait方法和notify/notifyAll方法在放棄對象監(jiān)視器時有什么區(qū)別?
wait()方法立即釋放對象監(jiān)視器;
notify()/notifyAll()方法則會等待線程剩余代碼執(zhí)行完畢才會放棄對象監(jiān)視器。
● Lock和synchronized對比?
(1)Lock是一個接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實(shí)現(xiàn);
(2)synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;
(3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應(yīng)中斷;
(4)通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
(5)Lock可以提高多個線程進(jìn)行讀操作的效率。
(6)在JDK1.5中,synchronized是性能低效的。因?yàn)檫@是一個重量級操作,它對性能最大的影響是阻塞式的實(shí)現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。但是,JDK1.6,發(fā)生了變化,對synchronize加入了很多優(yōu)化措施,有自適應(yīng)自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。導(dǎo)致在JDK1.6上synchronize的性能并不比Lock差。因此。提倡優(yōu)先考慮使用synchronized來進(jìn)行同步。
● ConcurrentHashMap的并發(fā)度是什么?
ConcurrentHashMap的并發(fā)度就是segment的大小,默認(rèn)為16,這意味著最多同時可以有16條線程操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優(yōu)勢。
● ReadWriteLock是什么?
首先明確一下,不是說ReentrantLock不好,只是ReentrantLock某些時候有局限。如果使用ReentrantLock,可能本身是為了防止線程A在寫數(shù)據(jù)、線程B在讀數(shù)據(jù)造成的數(shù)據(jù)不一致,但這樣,如果線程C在讀數(shù)據(jù)、線程D也在讀數(shù)據(jù),讀數(shù)據(jù)是不會改變數(shù)據(jù)的,沒有必要加鎖,但是還是加鎖了,降低了程序的性能。因?yàn)檫@個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖接口,ReentrantReadWriteLock是ReadWriteLock接口的一個具體實(shí)現(xiàn),實(shí)現(xiàn)了讀寫的分離, 讀鎖是共享的,寫鎖是獨(dú)占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能。
● 不可變對象對多線程有什么幫助
不可變對象保證了對象的內(nèi)存可見性,對不可變對象的讀取不需要進(jìn)行額外的同步手段,提升了代碼執(zhí)行效率。
● FutureTask是什么?
FutureTask表示一個異步運(yùn)算的任務(wù)。FutureTask里面可以傳入一個Callable的具體實(shí)現(xiàn)類,可以對這個異步運(yùn)算的任務(wù)的結(jié)果進(jìn)行等待獲取、判斷是否已經(jīng)完成、取消任務(wù)等操作。當(dāng)然,由于FutureTask也是Runnable接口的實(shí)現(xiàn)類,所以FutureTask也可以放入線程池中。
● Java中用到的線程調(diào)度算法是什么?
搶占式。一個線程用完CPU之后,操作系統(tǒng)會根據(jù)線程優(yōu)先級、線程饑餓情況等數(shù)據(jù)算出一個總的優(yōu)先級并分配下一個時間片給某個線程執(zhí)行。
● 怎么得到一個線程安全的單例模式?
餓漢本來就是線程安全的,就不再多說了。懶漢單例本身就是非線程安全的,可以使用雙檢鎖的方式實(shí)現(xiàn)線程安全的單例模式。雙檢鎖就是volatile+synchronized實(shí)現(xiàn)。
● 什么是樂觀鎖和悲觀鎖?
(1)樂觀鎖:樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫如果提供類似于write_condition機(jī)制的其實(shí)都是提供的樂觀鎖。
(2)悲觀鎖:對于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài),悲觀鎖認(rèn)為競爭總是會發(fā)生,因此每次對某資源進(jìn)行操作時,都會持有一個獨(dú)占的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。
● Java編寫一個會導(dǎo)致死鎖的程序?
(1)兩個線程里面分別持有兩個Object對象:lock1和lock2。這兩個lock作為同步代碼塊的鎖;
(2)線程1的run()方法中同步代碼塊先獲取lock1的對象鎖,Thread.sleep(xxx),時間不需要太多,50毫秒差不多了,然后接著獲取lock2的對象鎖。這么做主要是為了防止線程1啟動一下子就連續(xù)獲得了lock1和lock2兩個對象的對象鎖;
(3)線程2的run)(方法中同步代碼塊先獲取lock2的對象鎖,接著獲取lock1的對象鎖,當(dāng)然這時lock1的對象鎖已經(jīng)被線程1鎖持有,線程2肯定是要等待線程1釋放lock1的對象鎖的。
這樣,線程1”睡覺”睡完,線程2已經(jīng)獲取了lock2的對象鎖了,線程1此時嘗試獲取lock2的對象鎖,便被阻塞,此時一個死鎖就形成了。
● 如何確保N個線程可以同時訪問N個資源又不導(dǎo)致死鎖?
使用多線程的時候,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序,并強(qiáng)制線程按照指定的順序獲取鎖。因此,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會出現(xiàn)死鎖了。
● Thread.sleep(0)的作用是什么?
這個問題和上面那個問題是相關(guān)的,我就連在一起了。由于Java采用搶占式的線程調(diào)度算法,因此可能會出現(xiàn)某條線程常常獲取到CPU控制權(quán)的情況,為了讓某些優(yōu)先級比較低的線程也能獲取到CPU控制權(quán),可以使用Thread.sleep(0)手動觸發(fā)一次操作系統(tǒng)分配時間片的操作,這也是平衡CPU控制權(quán)的一種操作。
● 高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù)怎樣使用線程池?并發(fā)不高、任務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?并發(fā)高、業(yè)務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?
(1)高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù),線程池線程數(shù)可以設(shè)置為CPU核數(shù)+1,減少線程上下文的切換
(2)并發(fā)不高、任務(wù)執(zhí)行時間長的業(yè)務(wù)要區(qū)分開看:
假如是業(yè)務(wù)時間長集中在IO操作上,也就是IO密集型的任務(wù),因?yàn)镮O操作并不占用CPU,所以不要讓所有的CPU閑下來,可以加大線程池中的線程數(shù)目,讓CPU處理更多的業(yè)務(wù)
假如是業(yè)務(wù)時間長集中在計(jì)算操作上,也就是計(jì)算密集型任務(wù),這個就沒辦法了,和(1)一樣吧,線程池中的線程數(shù)設(shè)置得少一些,減少線程上下文的切換
(3)并發(fā)高、業(yè)務(wù)執(zhí)行時間長,解決這種類型任務(wù)的關(guān)鍵不在于線程池而在于整體架構(gòu)的設(shè)計(jì),看看這些業(yè)務(wù)里面某些數(shù)據(jù)是否能做緩存是第一步,增加服務(wù)器是第二步,至于線程池的設(shè)置,設(shè)置參考(2)。最后,業(yè)務(wù)執(zhí)行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務(wù)進(jìn)行拆分和解耦。
● 同步方法和同步塊,哪個是更好的選擇,你怎么看?
同步塊,這意味著同步塊之外的代碼是異步執(zhí)行的,這比同步整個方法更提升代碼的效率。請知道一條原則:同步的范圍越少越好。借著這一條,我額外提一點(diǎn),雖說同步的范圍越少越好,但是在Java虛擬機(jī)中還是存在著一種叫做 鎖粗化 的優(yōu)化方法,這種方法就是把同步范圍變大。這是有用的,比方說StringBuffer,它是一個線程安全的類,自然最常用的append()方法是一個同步方法,我們寫代碼的時候會反復(fù)append字符串,這意味著要進(jìn)行反復(fù)的加鎖->解鎖,這對性能不利,因?yàn)檫@意味著Java虛擬機(jī)在這條線程上要反復(fù)地在內(nèi)核態(tài)和用戶態(tài)之間進(jìn)行切換,因此Java虛擬機(jī)會將多次append方法調(diào)用的代碼進(jìn)行一個鎖粗化的操作,將多次的append的操作擴(kuò)展到append方法的頭尾,變成一個大的同步塊,這樣就減少了加鎖–>解鎖的次數(shù),有效地提升了代碼執(zhí)行的效率。
● 線程類的構(gòu)造方法、靜態(tài)塊是被哪個線程調(diào)用的
這是一個非常刁鉆和狡猾的問題。請記住:線程類的構(gòu)造方法、靜態(tài)塊是被new這個線程類所在的線程所調(diào)用的,而run方法里面的代碼才是被線程自身所調(diào)用的。如果說上面的說法讓你感到困惑,那么我舉個例子, 假設(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)用的
● Hashtable的size()方法中明明只有一條語句”return count”,為什么還要做同步?
這是我之前的一個困惑,不知道大家有沒有想過這個問題。某個方法中如果有多條語句,并且都在操作同一個類變量,那么在多線程環(huán)境下不加鎖,勢必會引發(fā)線程安全問題,這很好理解,但是size()方法明明只有一條語句,為什么還要加鎖?
關(guān)于這個問題,在慢慢地工作、學(xué)習(xí)中,有了理解,主要原因有兩點(diǎn):
(1) 同一時間只能有一條線程執(zhí)行固定類的同步方法,但是對于類的非同步方法,可以多條線程同時訪問 。所以,這樣就有問題了,可能線程A在執(zhí)行Hashtable的put方法添加數(shù)據(jù),線程B則可以正常調(diào)用size()方法讀取Hashtable中當(dāng)前元素的個數(shù),那讀取到的值可能不是最新的,可能線程A添加了完了數(shù)據(jù),但是沒有對size++,線程B就已經(jīng)讀取size了,那么對于線程B來說讀取到的size一定是不準(zhǔn)確的。而給size()方法加了同步之后,意味著線程B調(diào)用size()方法只有在線程A調(diào)用put方法完畢之后才可以調(diào)用,這樣就保證了線程安全性
(2) CPU執(zhí)行代碼,執(zhí)行的不是Java代碼,這點(diǎn)很關(guān)鍵,一定得記住 。Java代碼最終是被翻譯成匯編代碼執(zhí)行的,匯編代碼才是真正可以和硬件電路交互的代碼。 即使你看到Java代碼只有一行,甚至你看到Java代碼編譯之后生成的字節(jié)碼也只有一行,也不意味著對于底層來說這句語句的操作只有一個 。一句”return count”假設(shè)被翻譯成了三句匯編語句執(zhí)行,完全可能執(zhí)行完第一句,線程就切換了。
● 多線程有什么用?
(1)發(fā)揮多核CPU的優(yōu)勢
隨著工業(yè)的進(jìn)步,現(xiàn)在的筆記本、臺式機(jī)乃至商用的應(yīng)用服務(wù)器至少也都是雙核的,4核、8核甚至16核的也都不少見,如果是單線程的程序,那么在雙核CPU上就浪費(fèi)了50%,在4核CPU上就浪費(fèi)了75%。 單核CPU上所謂的”多線程”那是假的多線程,同一時間處理器只會處理一段邏輯,只不過線程之間切換得比較快,看著像多個線程”同時”運(yùn)行罷了 。多核CPU上的多線程才是真正的多線程,它能讓你的多段邏輯同時工作,多線程,可以真正發(fā)揮出多核CPU的優(yōu)勢來,達(dá)到充分利用CPU的目的。
(2)防止阻塞
從程序運(yùn)行效率的角度來看,單核CPU不但不會發(fā)揮出多線程的優(yōu)勢,反而會因?yàn)樵趩魏薈PU上運(yùn)行多線程導(dǎo)致線程上下文的切換,而降低程序整體的效率。但是單核CPU我們還是要應(yīng)用多線程,就是為了防止阻塞。試想,如果單核CPU使用單線程,那么只要這個線程阻塞了,比方說遠(yuǎn)程讀取某個數(shù)據(jù)吧,對端遲遲未返回又沒有設(shè)置超時時間,那么你的整個程序在數(shù)據(jù)返回回來之前就停止運(yùn)行了。多線程可以防止這個問題,多條線程同時運(yùn)行,哪怕一條線程的代碼執(zhí)行讀取數(shù)據(jù)阻塞,也不會影響其它任務(wù)的執(zhí)行。
● Semaphore有什么作用,信號量是什么?
Semaphore就是一個信號量,它的作用是限制某段代碼塊的并發(fā)數(shù)。Semaphore有一個構(gòu)造函數(shù),可以傳入一個int型整數(shù)n,表示某段代碼最多只有n個線程可以訪問,如果超出了n,那么請等待,等到某個線程執(zhí)行完畢這段代碼塊,下一個線程再進(jìn)入。由此可以看出如果Semaphore構(gòu)造函數(shù)中傳入的int型整數(shù)n=1,相當(dāng)于變成了一個synchronized了。
● 一個線程如果出現(xiàn)了運(yùn)行時異常會怎么樣?
如果這個異常沒有被捕獲的話,這個線程就停止執(zhí)行了。另外重要的一點(diǎn)是: 如果這個線程持有某個某個對象的監(jiān)視器,那么這個對象監(jiān)視器會被立即釋放。
● 為什么使用線程池?
避免頻繁地創(chuàng)建和銷毀線程,達(dá)到線程對象的重用。另外,使用線程池還可以根據(jù)項(xiàng)目靈活地控制并發(fā)的數(shù)目。
● synchronized和ReentrantLock的區(qū)別
synchronized是和if、else、for、while一樣的關(guān)鍵字,ReentrantLock是類,這是二者的本質(zhì)區(qū)別。既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴(kuò)展性體現(xiàn)在幾點(diǎn)上:
(1)ReentrantLock可以對獲取鎖的等待時間進(jìn)行設(shè)置,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的信息
(3)ReentrantLock可以靈活地實(shí)現(xiàn)多路通知
另外,二者的鎖機(jī)制其實(shí)也是不一樣的。ReentrantLock底層調(diào)用的是Unsafe的park方法加鎖,synchronized操作的是對象頭中mark word。
● 什么是多線程的上下文切換
多線程的上下文切換是指CPU控制權(quán)由一個已經(jīng)正在運(yùn)行的線程切換到另外一個就緒并等待獲取CPU執(zhí)行權(quán)的線程的過程。
● 如果你提交任務(wù)時,線程池隊(duì)列已滿,這時會發(fā)生什么
如果你使用的LinkedBlockingQueue,也就是無界隊(duì)列的話,沒關(guān)系,繼續(xù)添加任務(wù)到阻塞隊(duì)列中等待執(zhí)行,因?yàn)長inkedBlockingQueue可以近乎認(rèn)為是一個無窮大的隊(duì)列,可以無限存放任務(wù);如果你使用的是有界隊(duì)列比方說ArrayBlockingQueue的話,任務(wù)首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務(wù),默認(rèn)是AbortPolicy。
● 什么是CAS?
CAS,全稱為Compare and Set,即比較-設(shè)置。假設(shè)有三個操作數(shù): 內(nèi)存值V、舊的預(yù)期值A(chǔ)、要修改的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,才會將內(nèi)存值修改為B并返回true,否則什么都不做并返回false 。當(dāng)然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內(nèi)存中最新的那個值,否則舊的預(yù)期值A(chǔ)對某條線程來說,永遠(yuǎn)是一個不會變的值A(chǔ),只要某次CAS操作失敗,永遠(yuǎn)都不可能成功。
● 什么是AQS?
簡單說一下AQS,AQS全稱為AbstractQueuedSychronizer,翻譯過來應(yīng)該是抽象隊(duì)列同步器。
如果說java.util.concurrent的基礎(chǔ)是CAS的話,那么AQS就是整個Java并發(fā)包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS實(shí)際上以雙向隊(duì)列的形式連接所有的Entry,比方說ReentrantLock,所有等待的線程都被放在一個Entry中并連成雙向隊(duì)列,前面一個線程使用ReentrantLock好了,則雙向隊(duì)列實(shí)際上的第一個Entry開始運(yùn)行。
AQS定義了對雙向隊(duì)列所有的操作,而只開放了tryLock和tryRelease方法給開發(fā)者使用,開發(fā)者可以根據(jù)自己的實(shí)現(xiàn)重寫tryLock和tryRelease方法,以實(shí)現(xiàn)自己的并發(fā)功能。
● notify和notifyAll的區(qū)別?
notifyAll使所有原來在該對象上等待被喚醒的線程統(tǒng)統(tǒng)退出wait的狀態(tài),變成等待該對象上的鎖,一旦該對象被解鎖,他們就會去競爭。notify是通知其中一個線程,不會通知所有的線程。
● 你是如何合理配置線程池大小的?
首先,需要考慮到線程池所進(jìn)行的工作的性質(zhì):IO密集型?CPU密集型?
簡單的分析來看,如果是CPU密集型的任務(wù),我們應(yīng)該設(shè)置數(shù)目較小的線程數(shù),比如CPU數(shù)目加1。如果是IO密集型的任務(wù),則應(yīng)該設(shè)置可能多的線程數(shù),由于IO操作不占用CPU,所以,不能讓CPU閑下來。當(dāng)然,如果線程數(shù)目太多,那么線程切換所帶來的開銷又會對系統(tǒng)的響應(yīng)時間帶來影響。
在《linux多線程服務(wù)器端編程》中有一個思路,CPU計(jì)算和IO的阻抗匹配原則。如果線程池中的線程在執(zhí)行任務(wù)時,密集計(jì)算所占的時間比重為P(0 下面驗(yàn)證一下邊界條件的正確性:
假設(shè)C = 8,P = 1.0,線程池的任務(wù)完全是密集計(jì)算,那么T = 8。只要8個活動線程就能讓8個CPU飽和,再多也沒用了,因?yàn)镃PU資源已經(jīng)耗光了。
假設(shè)C = 8,P = 0.5,線程池的任務(wù)有一半是計(jì)算,有一半在等IO上,那么T = 16.考慮操作系統(tǒng)能靈活,合理調(diào)度sleeping/writing/running線程,那么大概16個“50%繁忙的線程”能讓8個CPU忙個不停。啟動更多的線程并不能提高吞吐量,反而因?yàn)樵黾由舷挛那袚Q的開銷而降低性能。
如果P < 0.2,這個公式就不適用了,T可以取一個固定值,比如 5*C。另外公式里的C不一定是CPU總數(shù),可以是“分配給這項(xiàng)任務(wù)的CPU數(shù)目”,比如在8核機(jī)器上分出4個核來做一項(xiàng)任務(wù),那么C=4
文章如何合理設(shè)置線程池大小里面提到了一個公式:
最佳線程數(shù)目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數(shù)目
比如平均每個線程CPU運(yùn)行時間為0.5s,而線程等待時間(非CPU運(yùn)行時間,比如IO)為1.5s,CPU核心數(shù)為8,那么根據(jù)上面這個公式估算得到:((0.5+1.5)/0.5)*8=32。這個公式進(jìn)一步轉(zhuǎn)化為:
最佳線程數(shù)目 = (線程等待時間與線程CPU時間之比 + 1)* CPU數(shù)目
可以得出一個結(jié)論:
線程等待時間所占比例越高,需要越多線程。線程CPU時間所占比例越高,需要越少線程。
以上公式與之前的CPU和IO密集型任務(wù)設(shè)置線程數(shù)基本吻合。
● 線程池具體配置參數(shù)?
corePoolSize:核心線程數(shù)
queueCapacity:任務(wù)隊(duì)列容量(阻塞隊(duì)列)
maxPoolSize:最大線程數(shù)
keepAliveTime:線程空閑時間
allowCoreThreadTimeout:允許核心線程超時
rejectedExecutionHandler:任務(wù)拒絕處理器
● 線程池按以下行為執(zhí)行任務(wù):
(1)當(dāng)線程數(shù)小于核心線程數(shù)時,創(chuàng)建線程。
(2)當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊(duì)列未滿時,將任務(wù)放入任務(wù)隊(duì)列。
(3)當(dāng)線程數(shù)大于等于核心線程數(shù),且任務(wù)隊(duì)列已滿
若線程數(shù)小于最大線程數(shù),創(chuàng)建線程
若線程數(shù)等于最大線程數(shù),拋出異常,拒絕任務(wù)