【2022】多线程并发编程面试真题
文章目錄
- 4. 多線程
- 4.1 創(chuàng)建線程有哪幾種方式?
- 4.2 說(shuō)說(shuō)Thread類的常用方法
- 4.3 run()和start()有什么區(qū)別?
- 4.4 線程是否可以重復(fù)啟動(dòng),會(huì)有什么后果?
- 4.5 介紹一下線程的生命周期
- 4.6 如何實(shí)現(xiàn)線程同步?
- 4.7 說(shuō)一說(shuō)Java多線程之間的通信方式
- 4.8 說(shuō)一說(shuō)Java同步機(jī)制中的wait和notify
- 4.9 說(shuō)一說(shuō)sleep()和wait()的區(qū)別
- 4.10 說(shuō)一說(shuō)notify()、notifyAll()的區(qū)別
- 4.11 如何實(shí)現(xiàn)子線程先執(zhí)行,主線程再執(zhí)行?
- 4.12 阻塞線程的方式有哪些?
- 4.13 說(shuō)一說(shuō)synchronized與Lock的區(qū)別
- 4.14 說(shuō)一說(shuō)synchronized的底層實(shí)現(xiàn)原理
- 4.15 synchronized可以修飾靜態(tài)方法和靜態(tài)代碼塊嗎?
- 4.16 談?wù)凴eentrantLock的實(shí)現(xiàn)原理
- 4.17 如果不使用synchronized和Lock,如何保證線程安全?
- 4.18 說(shuō)一說(shuō)Java中樂(lè)觀鎖和悲觀鎖的區(qū)別
- 4.19 公平鎖與非公平鎖是怎么實(shí)現(xiàn)的?
- 4.20 了解Java中的鎖升級(jí)嗎?
- 4.21 如何實(shí)現(xiàn)互斥鎖(mutex)?
- 4.22 分段鎖是怎么實(shí)現(xiàn)的?
- 4.23 說(shuō)說(shuō)你對(duì)讀寫鎖的了解
- 4.24 volatile關(guān)鍵字有什么用?
- 4.25 談?wù)剉olatile的實(shí)現(xiàn)原理
- 4.26 說(shuō)說(shuō)你對(duì)JUC的了解
- 4.27 說(shuō)說(shuō)你對(duì)AQS的理解
- 4.28 LongAdder解決了什么問(wèn)題,它是如何實(shí)現(xiàn)的?
- 4.29 介紹下ThreadLocal和它的應(yīng)用場(chǎng)景
- 4.30 請(qǐng)介紹ThreadLocal的實(shí)現(xiàn)原理,它是怎么處理hash沖突的?
- 4.31 介紹一下線程池
- 4.32 介紹一下線程池的工作流程
- 4.33 線程池都有哪些狀態(tài)?
- 4.34 談?wù)劸€程池的拒絕策略
- 4.35 線程池的隊(duì)列大小你通常怎么設(shè)置?
- 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?
- 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?
4. 多線程
4.1 創(chuàng)建線程有哪幾種方式?
參考答案
創(chuàng)建線程有三種方式,分別是繼承Thread類、實(shí)現(xiàn)Runnable接口、實(shí)現(xiàn)Callable接口。
通過(guò)繼承Thread類來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:
通過(guò)實(shí)現(xiàn)Runnable接口來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:
通過(guò)實(shí)現(xiàn)Callable接口來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:
擴(kuò)展閱讀
通過(guò)繼承Thread類、實(shí)現(xiàn)Runnable接口、實(shí)現(xiàn)Callable接口都可以實(shí)現(xiàn)多線程,不過(guò)實(shí)現(xiàn)Runnable接口與實(shí)現(xiàn)Callable接口的方式基本相同,只是Callable接口里定義的方法有返回值,可以聲明拋出異常而已。因此可以將實(shí)現(xiàn)Runnable接口和實(shí)現(xiàn)Callable接口歸為一種方式。
采用實(shí)現(xiàn)Runnable、Callable接口的方式創(chuàng)建多線程的優(yōu)缺點(diǎn):
- 線程類只是實(shí)現(xiàn)了Runnable接口或Callable接口,還可以繼承其他類。
- 在這種方式下,多個(gè)線程可以共享同一個(gè)target對(duì)象,所以非常適合多個(gè)相同線程來(lái)處理同一份資源的情況,從而可以將CPU、代碼和數(shù)據(jù)分開,形成清晰的模型,較好地體現(xiàn)了面向?qū)ο蟮乃枷搿?/li>
- 劣勢(shì)是,編程稍稍復(fù)雜,如果需要訪問(wèn)當(dāng)前線程,則必須使用Thread.currentThread()方法。
采用繼承Thread類的方式創(chuàng)建多線程的優(yōu)缺點(diǎn):
- 劣勢(shì)是,因?yàn)榫€程類已經(jīng)繼承了Thread類,所以不能再繼承其他父類。
- 優(yōu)勢(shì)是,編寫簡(jiǎn)單,如果需要訪問(wèn)當(dāng)前線程,則無(wú)須使用Thread.currentThread()方法,直接使用this即可獲得當(dāng)前線程。
鑒于上面分析,因此一般推薦采用實(shí)現(xiàn)Runnable接口、Callable接口的方式來(lái)創(chuàng)建多線程。
4.2 說(shuō)說(shuō)Thread類的常用方法
參考答案
Thread類常用構(gòu)造方法:
- Thread()
- Thread(String name)
- Thread(Runnable target)
- Thread(Runnable target, String name)
其中,參數(shù) name為線程名,參數(shù) target為包含線程體的目標(biāo)對(duì)象。
Thread類常用靜態(tài)方法:
- currentThread():返回當(dāng)前正在執(zhí)行的線程;
- interrupted():返回當(dāng)前執(zhí)行的線程是否已經(jīng)被中斷;
- sleep(long millis):使當(dāng)前執(zhí)行的線程睡眠多少毫秒數(shù);
- yield():使當(dāng)前執(zhí)行的線程自愿暫時(shí)放棄對(duì)處理器的使用權(quán)并允許其他線程執(zhí)行;
Thread類常用實(shí)例方法:
- getId():返回該線程的id;
- getName():返回該線程的名字;
- getPriority():返回該線程的優(yōu)先級(jí);
- interrupt():使該線程中斷;
- isInterrupted():返回該線程是否被中斷;
- isAlive():返回該線程是否處于活動(dòng)狀態(tài);
- isDaemon():返回該線程是否是守護(hù)線程;
- setDaemon(boolean on):將該線程標(biāo)記為守護(hù)線程或用戶線程,如果不標(biāo)記默認(rèn)是非守護(hù)線程;
- setName(String name):設(shè)置該線程的名字;
- setPriority(int newPriority):改變?cè)摼€程的優(yōu)先級(jí);
- join():等待該線程終止;
- join(long millis):等待該線程終止,至多等待多少毫秒數(shù)。
4.3 run()和start()有什么區(qū)別?
參考答案
run()方法被稱為線程執(zhí)行體,它的方法體代表了線程需要完成的任務(wù),而start()方法用來(lái)啟動(dòng)線程。
調(diào)用start()方法啟動(dòng)線程時(shí),系統(tǒng)會(huì)把該run()方法當(dāng)成線程執(zhí)行體來(lái)處理。但如果直接調(diào)用線程對(duì)象的run()方法,則run()方法立即就會(huì)被執(zhí)行,而且在run()方法返回之前其他線程無(wú)法并發(fā)執(zhí)行。也就是說(shuō),如果直接調(diào)用線程對(duì)象的run()方法,系統(tǒng)把線程對(duì)象當(dāng)成一個(gè)普通對(duì)象,而run()方法也是一個(gè)普通方法,而不是線程執(zhí)行體。
4.4 線程是否可以重復(fù)啟動(dòng),會(huì)有什么后果?
參考答案
只能對(duì)處于新建狀態(tài)的線程調(diào)用start()方法,否則將引發(fā)IllegalThreadStateException異常。
擴(kuò)展閱讀
當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個(gè)線程之后,該線程就處于新建狀態(tài),此時(shí)它和其他的Java對(duì)象一樣,僅僅由Java虛擬機(jī)為其分配內(nèi)存,并初始化其成員變量的值。此時(shí)的線程對(duì)象沒(méi)有表現(xiàn)出任何線程的動(dòng)態(tài)特征,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體。
當(dāng)線程對(duì)象調(diào)用了start()方法之后,該線程處于就緒狀態(tài),Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器,處于這個(gè)狀態(tài)中的線程并沒(méi)有開始運(yùn)行,只是表示該線程可以運(yùn)行了。至于該線程何時(shí)開始運(yùn)行,取決于JVM里線程調(diào)度器的調(diào)度。
4.5 介紹一下線程的生命周期
參考答案
在線程的生命周期中,它要經(jīng)過(guò)新建(New)、就緒(Ready)、運(yùn)行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態(tài)。尤其是當(dāng)線程啟動(dòng)以后,它不可能一直“霸占”著CPU獨(dú)自運(yùn)行,所以CPU需要在多條線程之間切換,于是線程狀態(tài)也會(huì)多次在運(yùn)行、就緒之間切換。
當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個(gè)線程之后,該線程就處于新建狀態(tài),此時(shí)它和其他的Java對(duì)象一樣,僅僅由Java虛擬機(jī)為其分配內(nèi)存,并初始化其成員變量的值。此時(shí)的線程對(duì)象沒(méi)有表現(xiàn)出任何線程的動(dòng)態(tài)特征,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體。
當(dāng)線程對(duì)象調(diào)用了start()方法之后,該線程處于就緒狀態(tài),Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器,處于這個(gè)狀態(tài)中的線程并沒(méi)有開始運(yùn)行,只是表示該線程可以運(yùn)行了。至于該線程何時(shí)開始運(yùn)行,取決于JVM里線程調(diào)度器的調(diào)度。
如果處于就緒狀態(tài)的線程獲得了CPU,開始執(zhí)行run()方法的線程執(zhí)行體,則該線程處于運(yùn)行狀態(tài),如果計(jì)算機(jī)只有一個(gè)CPU,那么在任何時(shí)刻只有一個(gè)線程處于運(yùn)行狀態(tài)。當(dāng)然,在一個(gè)多處理器的機(jī)器上,將會(huì)有多個(gè)線程并行執(zhí)行;當(dāng)線程數(shù)大于處理器數(shù)時(shí),依然會(huì)存在多個(gè)線程在同一個(gè)CPU上輪換的現(xiàn)象。
當(dāng)一個(gè)線程開始運(yùn)行后,它不可能一直處于運(yùn)行狀態(tài),線程在運(yùn)行過(guò)程中需要被中斷,目的是使其他線程獲得執(zhí)行的機(jī)會(huì),線程調(diào)度的細(xì)節(jié)取決于底層平臺(tái)所采用的策略。對(duì)于采用搶占式策略的系統(tǒng)而言,系統(tǒng)會(huì)給每個(gè)可執(zhí)行的線程一個(gè)小時(shí)間段來(lái)處理任務(wù)。當(dāng)該時(shí)間段用完后,系統(tǒng)就會(huì)剝奪該線程所占用的資源,讓其他線程獲得執(zhí)行的機(jī)會(huì)。當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):
- 線程調(diào)用sleep()方法主動(dòng)放棄所占用的處理器資源。
- 線程調(diào)用了一個(gè)阻塞式IO方法,在該方法返回之前,該線程被阻塞。
- 線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有。
- 線程在等待某個(gè)通知(notify)。
- 程序調(diào)用了線程的suspend()方法將該線程掛起。但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法。
針對(duì)上面幾種情況,當(dāng)發(fā)生如下特定的情況時(shí)可以解除上面的阻塞,讓該線程重新進(jìn)入就緒狀態(tài):
- 調(diào)用sleep()方法的線程經(jīng)過(guò)了指定時(shí)間。
- 線程調(diào)用的阻塞式IO方法已經(jīng)返回。
- 線程成功地獲得了試圖取得的同步監(jiān)視器。
- 線程正在等待某個(gè)通知時(shí),其他線程發(fā)出了一個(gè)通知。
- 處于掛起狀態(tài)的線程被調(diào)用了resume()恢復(fù)方法。
線程會(huì)以如下三種方式結(jié)束,結(jié)束后就處于死亡狀態(tài):
- run()或call()方法執(zhí)行完成,線程正常結(jié)束。
- 線程拋出一個(gè)未捕獲的Exception或Error。
- 直接調(diào)用該線程的stop()方法來(lái)結(jié)束該線程,該方法容易導(dǎo)致死鎖,通常不推薦使用。
擴(kuò)展閱讀
線程5種狀態(tài)的轉(zhuǎn)換關(guān)系,如下圖所示:
4.6 如何實(shí)現(xiàn)線程同步?
參考答案
同步方法
即有synchronized關(guān)鍵字修飾的方法,由于java的每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖,當(dāng)用此關(guān)鍵字修飾方法時(shí), 內(nèi)置鎖會(huì)保護(hù)整個(gè)方法。在調(diào)用該方法前,需要獲得內(nèi)置鎖,否則就處于阻塞狀態(tài)。需要注意, synchronized關(guān)鍵字也可以修飾靜態(tài)方法,此時(shí)如果調(diào)用該靜態(tài)方法,將會(huì)鎖住整個(gè)類。
同步代碼塊
即有synchronized關(guān)鍵字修飾的語(yǔ)句塊,被該關(guān)鍵字修飾的語(yǔ)句塊會(huì)自動(dòng)被加上內(nèi)置鎖,從而實(shí)現(xiàn)同步。需值得注意的是,同步是一種高開銷的操作,因此應(yīng)該盡量減少同步的內(nèi)容。通常沒(méi)有必要同步整個(gè)方法,使用synchronized代碼塊同步關(guān)鍵代碼即可。
ReentrantLock
Java 5新增了一個(gè)java.util.concurrent包來(lái)支持同步,其中ReentrantLock類是可重入、互斥、實(shí)現(xiàn)了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語(yǔ)義,并且擴(kuò)展了其能力。需要注意的是,ReentrantLock還有一個(gè)可以創(chuàng)建公平鎖的構(gòu)造方法,但由于能大幅度降低程序運(yùn)行效率,因此不推薦使用。
volatile
volatile關(guān)鍵字為域變量的訪問(wèn)提供了一種免鎖機(jī)制,使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能會(huì)被其他線程更新,因此每次使用該域就要重新計(jì)算,而不是使用寄存器中的值。需要注意的是,volatile不會(huì)提供任何原子操作,它也不能用來(lái)修飾final類型的變量。
原子變量
在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡(jiǎn)化線程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在應(yīng)用程序中(如以原子方式增加的計(jì)數(shù)器),但不能用于替換Integer。可擴(kuò)展Number,允許那些處理機(jī)遇數(shù)字類的工具和實(shí)用工具進(jìn)行統(tǒng)一訪問(wèn)。
4.7 說(shuō)一說(shuō)Java多線程之間的通信方式
參考答案
在Java中線程通信主要有以下三種方式:
wait()、notify()、notifyAll()
如果線程之間采用synchronized來(lái)保證線程安全,則可以利用wait()、notify()、notifyAll()來(lái)實(shí)現(xiàn)線程通信。這三個(gè)方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個(gè)對(duì)象都擁有鎖,所以讓當(dāng)前線程等待某個(gè)對(duì)象的鎖,當(dāng)然應(yīng)該通過(guò)這個(gè)對(duì)象來(lái)操作。并且因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖,如果通過(guò)線程來(lái)操作,就非常復(fù)雜了。另外,這三個(gè)方法都是本地方法,并且被final修飾,無(wú)法被重寫。
wait()方法可以讓當(dāng)前線程釋放對(duì)象鎖并進(jìn)入阻塞狀態(tài)。notify()方法用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。notifyAll()用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。
每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列。就緒隊(duì)列存儲(chǔ)了已就緒(將要競(jìng)爭(zhēng)鎖)的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程。當(dāng)一個(gè)阻塞線程被喚醒后,才會(huì)進(jìn)入就緒隊(duì)列,進(jìn)而等待CPU的調(diào)度。反之,當(dāng)一個(gè)線程被wait后,就會(huì)進(jìn)入阻塞隊(duì)列,等待被喚醒。
await()、signal()、signalAll()
如果線程之間采用Lock來(lái)保證線程安全,則可以利用await()、signal()、signalAll()來(lái)實(shí)現(xiàn)線程通信。這三個(gè)方法都是Condition接口中的方法,該接口是在Java 1.5中出現(xiàn)的,它用來(lái)替代傳統(tǒng)的wait+notify實(shí)現(xiàn)線程間的協(xié)作,它的使用依賴于 Lock。相比使用wait+notify,使用Condition的await+signal這種方式能夠更加安全和高效地實(shí)現(xiàn)線程間協(xié)作。
Condition依賴于Lock接口,生成一個(gè)Condition的基本代碼是lock.newCondition() 。 必須要注意的是,Condition 的 await()/signal()/signalAll() 使用都必須在lock保護(hù)之內(nèi),也就是說(shuō),必須在lock.lock()和lock.unlock之間才可以使用。事實(shí)上,await()/signal()/signalAll() 與 wait()/notify()/notifyAll()有著天然的對(duì)應(yīng)關(guān)系。即:Conditon中的await()對(duì)應(yīng)Object的wait(),Condition中的signal()對(duì)應(yīng)Object的notify(),Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()。
BlockingQueue
Java 5提供了一個(gè)BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要用途并不是作為容器,而是作為線程通信的工具。BlockingQueue具有一個(gè)特征:當(dāng)生產(chǎn)者線程試圖向BlockingQueue中放入元素時(shí),如果該隊(duì)列已滿,則該線程被阻塞;當(dāng)消費(fèi)者線程試圖從BlockingQueue中取出元素時(shí),如果該隊(duì)列已空,則該線程被阻塞。
程序的兩個(gè)線程通過(guò)交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。線程之間需要通信,最經(jīng)典的場(chǎng)景就是生產(chǎn)者與消費(fèi)者模型,而BlockingQueue就是針對(duì)該模型提供的解決方案。
4.8 說(shuō)一說(shuō)Java同步機(jī)制中的wait和notify
參考答案
wait()、notify()、notifyAll()用來(lái)實(shí)現(xiàn)線程之間的通信,這三個(gè)方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個(gè)對(duì)象都擁有鎖,所以讓當(dāng)前線程等待某個(gè)對(duì)象的鎖,當(dāng)然應(yīng)該通過(guò)這個(gè)對(duì)象來(lái)操作。并且因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖,如果通過(guò)線程來(lái)操作,就非常復(fù)雜了。另外,這三個(gè)方法都是本地方法,并且被final修飾,無(wú)法被重寫,并且只有采用synchronized實(shí)現(xiàn)線程同步時(shí)才能使用這三個(gè)方法。
wait()方法可以讓當(dāng)前線程釋放對(duì)象鎖并進(jìn)入阻塞狀態(tài)。notify()方法用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。notifyAll()方法用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。
每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列。就緒隊(duì)列存儲(chǔ)了已就緒(將要競(jìng)爭(zhēng)鎖)的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程。當(dāng)一個(gè)阻塞線程被喚醒后,才會(huì)進(jìn)入就緒隊(duì)列,進(jìn)而等待CPU的調(diào)度。反之,當(dāng)一個(gè)線程被wait后,就會(huì)進(jìn)入阻塞隊(duì)列,等待被喚醒。
4.9 說(shuō)一說(shuō)sleep()和wait()的區(qū)別
參考答案
4.10 說(shuō)一說(shuō)notify()、notifyAll()的區(qū)別
參考答案
-
notify()
用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。
-
notifyAll()
用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。
4.11 如何實(shí)現(xiàn)子線程先執(zhí)行,主線程再執(zhí)行?
參考答案
啟動(dòng)子線程后,立即調(diào)用該線程的join()方法,則主線程必須等待子線程執(zhí)行完成后再執(zhí)行。
擴(kuò)展閱讀
Thread類提供了讓一個(gè)線程等待另一個(gè)線程完成的方法——join()方法。當(dāng)在某個(gè)程序執(zhí)行流中調(diào)用其他線程的join()方法時(shí),調(diào)用線程將被阻塞,直到被join()方法加入的join線程執(zhí)行完為止。
join()方法通常由使用線程的程序調(diào)用,以將大問(wèn)題劃分成許多小問(wèn)題,每個(gè)小問(wèn)題分配一個(gè)線程。當(dāng)所有的小問(wèn)題都得到處理后,再調(diào)用主線程來(lái)進(jìn)一步操作。
4.12 阻塞線程的方式有哪些?
參考答案
當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):
- 線程調(diào)用sleep()方法主動(dòng)放棄所占用的處理器資源;
- 線程調(diào)用了一個(gè)阻塞式IO方法,在該方法返回之前,該線程被阻塞;
- 線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有;
- 線程在等待某個(gè)通知(notify);
- 程序調(diào)用了線程的suspend()方法將該線程掛起,但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法。
4.13 說(shuō)一說(shuō)synchronized與Lock的區(qū)別
參考答案
4.14 說(shuō)一說(shuō)synchronized的底層實(shí)現(xiàn)原理
參考答案
一、以下列代碼為例,說(shuō)明同步代碼塊的底層實(shí)現(xiàn)原理:
public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("Method 1 start");}} }查看反編譯后結(jié)果,如下圖:
可見,synchronized作用在代碼塊時(shí),它的底層是通過(guò)monitorenter、monitorexit指令來(lái)實(shí)現(xiàn)的。
-
monitorenter:
每個(gè)對(duì)象都是一個(gè)監(jiān)視器鎖(monitor),當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過(guò)程如下:
如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
-
monitorexit:
執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor持有者。指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)monitor的所有權(quán)。
monitorexit指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖,第2次為發(fā)生異步退出釋放鎖。
二、以下列代碼為例,說(shuō)明同步方法的底層實(shí)現(xiàn)原理:
public class SynchronizedMethod {public synchronized void method() {System.out.println("Hello World!");} }查看反編譯后結(jié)果,如下圖:
從反編譯的結(jié)果來(lái)看,方法的同步并沒(méi)有通過(guò) monitorenter 和 monitorexit 指令來(lái)完成,不過(guò)相對(duì)于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來(lái)實(shí)現(xiàn)方法的同步的:
當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法再獲得同一個(gè)monitor對(duì)象。
三、總結(jié)
兩種同步方式本質(zhì)上沒(méi)有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成。兩個(gè)指令的執(zhí)行是JVM通過(guò)調(diào)用操作系統(tǒng)的互斥原語(yǔ)mutex來(lái)實(shí)現(xiàn),被阻塞的線程會(huì)被掛起、等待重新調(diào)度,會(huì)導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個(gè)態(tài)之間來(lái)回切換,對(duì)性能有較大影響。
4.15 synchronized可以修飾靜態(tài)方法和靜態(tài)代碼塊嗎?
參考答案
synchronized可以修飾靜態(tài)方法,但不能修飾靜態(tài)代碼塊。
當(dāng)修飾靜態(tài)方法時(shí),監(jiān)視器鎖(monitor)便是對(duì)象的Class實(shí)例,因?yàn)镃lass數(shù)據(jù)存在于永久代,因此靜態(tài)方法鎖相當(dāng)于該類的一個(gè)全局鎖。
4.16 談?wù)凴eentrantLock的實(shí)現(xiàn)原理
參考答案
ReentrantLock是基于AQS實(shí)現(xiàn)的,AQS即AbstractQueuedSynchronizer的縮寫,這個(gè)是個(gè)內(nèi)部實(shí)現(xiàn)了兩個(gè)隊(duì)列的抽象類,分別是同步隊(duì)列和條件隊(duì)列。其中同步隊(duì)列是一個(gè)雙向鏈表,里面儲(chǔ)存的是處于等待狀態(tài)的線程,正在排隊(duì)等待喚醒去獲取鎖,而條件隊(duì)列是一個(gè)單向鏈表,里面儲(chǔ)存的也是處于等待狀態(tài)的線程,只不過(guò)這些線程喚醒的結(jié)果是加入到了同步隊(duì)列的隊(duì)尾,AQS所做的就是管理這兩個(gè)隊(duì)列里面線程之間的等待狀態(tài)-喚醒的工作。
在同步隊(duì)列中,還存在2中模式,分別是獨(dú)占模式和共享模式,這兩種模式的區(qū)別就在于AQS在喚醒線程節(jié)點(diǎn)的時(shí)候是不是傳遞喚醒,這兩種模式分別對(duì)應(yīng)獨(dú)占鎖和共享鎖。
AQS是一個(gè)抽象類,所以不能直接實(shí)例化,當(dāng)我們需要實(shí)現(xiàn)一個(gè)自定義鎖的時(shí)候可以去繼承AQS然后重寫獲取鎖的方式和釋放鎖的方式還有管理state,而ReentrantLock就是通過(guò)重寫了AQS的tryAcquire和tryRelease方法實(shí)現(xiàn)的lock和unlock。
ReentrantLock 結(jié)構(gòu)如下圖所示:
首先ReentrantLock 實(shí)現(xiàn)了 Lock 接口,然后有3個(gè)內(nèi)部類,其中Sync內(nèi)部類繼承自AQS,另外的兩個(gè)內(nèi)部類繼承自Sync,這兩個(gè)類分別是用來(lái)公平鎖和非公平鎖的。通過(guò)Sync重寫的方法tryAcquire、tryRelease可以知道,ReentrantLock實(shí)現(xiàn)的是AQS的獨(dú)占模式,也就是獨(dú)占鎖,這個(gè)鎖是悲觀鎖。
4.17 如果不使用synchronized和Lock,如何保證線程安全?
參考答案
volatile
volatile關(guān)鍵字為域變量的訪問(wèn)提供了一種免鎖機(jī)制,使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能會(huì)被其他線程更新,因此每次使用該域就要重新計(jì)算,而不是使用寄存器中的值。需要注意的是,volatile不會(huì)提供任何原子操作,它也不能用來(lái)修飾final類型的變量。
原子變量
在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡(jiǎn)化線程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在應(yīng)用程序中(如以原子方式增加的計(jì)數(shù)器),但不能用于替換Integer。可擴(kuò)展Number,允許那些處理機(jī)遇數(shù)字類的工具和實(shí)用工具進(jìn)行統(tǒng)一訪問(wèn)。
本地存儲(chǔ)
可以通過(guò)ThreadLocal類來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)的功能。每一個(gè)線程的Thread對(duì)象中都有一個(gè)ThreadLocalMap對(duì)象,這個(gè)對(duì)象存儲(chǔ)了一組以ThreadLocal.threadLocalHashCode為鍵,以本地線程變量為值的K-V值對(duì),ThreadLocal對(duì)象就是當(dāng)前線程的ThreadLocalMap的訪問(wèn)入口,每一個(gè)ThreadLocal對(duì)象都包含了一個(gè)獨(dú)一無(wú)二的threadLocalHashCode值,使用這個(gè)值就可以在線程K-V值對(duì)中找回對(duì)應(yīng)的本地線程變量。
不可變的
只要一個(gè)不可變的對(duì)象被正確地構(gòu)建出來(lái),那其外部的可見狀態(tài)永遠(yuǎn)都不會(huì)改變,永遠(yuǎn)都不會(huì)看到它在多個(gè)線程之中處于不一致的狀態(tài),“不可變”帶來(lái)的安全性是最直接、最純粹的。Java語(yǔ)言中,如果多線程共享的數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型,那么只要在定義時(shí)使用final關(guān)鍵字修飾它就可以保證它是不可變的。如果共享數(shù)據(jù)是一個(gè)對(duì)象,由于Java語(yǔ)言目前暫時(shí)還沒(méi)有提供值類型的支持,那就需要對(duì)象自行保證其行為不會(huì)對(duì)其狀態(tài)產(chǎn)生任何影響才行。String類是一個(gè)典型的不可變類,可以參考它設(shè)計(jì)一個(gè)不可變類。
4.18 說(shuō)一說(shuō)Java中樂(lè)觀鎖和悲觀鎖的區(qū)別
參考答案
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。Java中悲觀鎖是通過(guò)synchronized關(guān)鍵字或Lock接口來(lái)實(shí)現(xiàn)的。
樂(lè)觀鎖:顧名思義,就是很樂(lè)觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒(méi)有去更新這個(gè)數(shù)據(jù)。樂(lè)觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相對(duì)于對(duì)于 synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實(shí)現(xiàn)。所以J.U.C在性能上有了很大的提升。
4.19 公平鎖與非公平鎖是怎么實(shí)現(xiàn)的?
參考答案
在Java中實(shí)現(xiàn)鎖的方式有兩種,一種是使用Java自帶的關(guān)鍵字synchronized對(duì)相應(yīng)的類或者方法以及代碼塊進(jìn)行加鎖,另一種是ReentrantLock,前者只能是非公平鎖,而后者是默認(rèn)非公平但可實(shí)現(xiàn)公平的一把鎖。
ReentrantLock是基于其內(nèi)部類FairSync(公平鎖)和NonFairSync(非公平鎖)實(shí)現(xiàn)的,并且它的實(shí)現(xiàn)依賴于Java同步器框架AbstractQueuedSynchronizer(AQS),AQS使用一個(gè)整形的volatile變量state來(lái)維護(hù)同步狀態(tài),這個(gè)volatile變量是實(shí)現(xiàn)ReentrantLock的關(guān)鍵。我們來(lái)看一下ReentrantLock的類圖:
ReentrantLock 的公平鎖和非公平鎖都委托了 AbstractQueuedSynchronizer#acquire 去請(qǐng)求獲取。
public` `final` `void` `acquire(``int` `arg) {`` ``if` `(!tryAcquire(arg) &&`` ``acquireQueued(addWaiter(Node.EXCLUSIVE), arg))`` ``selfInterrupt();``}- tryAcquire 是一個(gè)抽象方法,是公平與非公平的實(shí)現(xiàn)原理所在。
- addWaiter 是將當(dāng)前線程結(jié)點(diǎn)加入等待隊(duì)列之中。公平鎖在鎖釋放后會(huì)嚴(yán)格按照等到隊(duì)列去取后續(xù)值,而非公平鎖在對(duì)于新晉線程有很大優(yōu)勢(shì)。
- acquireQueued 在多次循環(huán)中嘗試獲取到鎖或者將當(dāng)前線程阻塞。
- selfInterrupt 如果線程在阻塞期間發(fā)生了中斷,調(diào)用 Thread.currentThread().interrupt() 中斷當(dāng)前線程。
公平鎖和非公平鎖在說(shuō)的獲取上都使用到了 volatile 關(guān)鍵字修飾的state字段, 這是保證多線程環(huán)境下鎖的獲取與否的核心。但是當(dāng)并發(fā)情況下多個(gè)線程都讀取到 state == 0時(shí),則必須用到CAS技術(shù),一門CPU的原子鎖技術(shù),可通過(guò)CPU對(duì)共享變量加鎖的形式,實(shí)現(xiàn)數(shù)據(jù)變更的原子操作。volatile 和 CAS的結(jié)合是并發(fā)搶占的關(guān)鍵。
-
公平鎖FairSync
公平鎖的實(shí)現(xiàn)機(jī)理在于每次有線程來(lái)?yè)屨兼i的時(shí)候,都會(huì)檢查一遍有沒(méi)有等待隊(duì)列,如果有, 當(dāng)前線程會(huì)執(zhí)行如下步驟:
if (!hasQueuedPredecessors() && compareAndSetState(``0``, acquires)) { `` ``setExclusiveOwnerThread(current);`` ``return true``;``}
其中hasQueuedPredecessors是用于檢查是否有等待隊(duì)列的:
public final boolean hasQueuedPredecessors() {`` ``Node t = tail; ``// Read fields in reverse initialization order`` ``Node h = head;`` ``Node s;`` ``return h != t &&`` ``((s = h.next) == ``null || s.thread != Thread.currentThread());``}
-
非公平鎖NonfairSync
非公平鎖在實(shí)現(xiàn)的時(shí)候多次強(qiáng)調(diào)隨機(jī)搶占:
if (c == ``0``) {`` ``if (compareAndSetState(``0``, acquires)) {`` ``setExclusiveOwnerThread(current);`` ``return true``;`` ``}``}
與公平鎖的區(qū)別在于新晉獲取鎖的進(jìn)程會(huì)有多次機(jī)會(huì)去搶占鎖,被加入了等待隊(duì)列后則跟公平鎖沒(méi)有區(qū)別。
4.20 了解Java中的鎖升級(jí)嗎?
參考答案
JDK 1.6之前,synchronized 還是一個(gè)重量級(jí)鎖,是一個(gè)效率比較低下的鎖。但是在JDK 1.6后,JVM為了提高鎖的獲取與釋放效率對(duì)synchronized 進(jìn)行了優(yōu)化,引入了偏向鎖和輕量級(jí)鎖 ,從此以后鎖的狀態(tài)就有了四種:無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。并且四種狀態(tài)會(huì)隨著競(jìng)爭(zhēng)的情況逐漸升級(jí),而且是不可逆的過(guò)程,即不可降級(jí),這四種鎖的級(jí)別由低到高依次是:無(wú)鎖、偏向鎖,輕量級(jí)鎖,重量級(jí)鎖。如下圖所示:
無(wú)鎖
無(wú)鎖是指沒(méi)有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問(wèn)并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功。無(wú)鎖的特點(diǎn)是修改操作會(huì)在循環(huán)內(nèi)進(jìn)行,線程會(huì)不斷的嘗試修改共享資源。如果沒(méi)有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功。
偏向鎖
初次執(zhí)行到synchronized代碼塊的時(shí)候,鎖對(duì)象變成偏向鎖(通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位),字面意思是“偏向于第一個(gè)獲得它的線程”的鎖。執(zhí)行完同步代碼塊后,線程并不會(huì)主動(dòng)釋放偏向鎖。當(dāng)?shù)诙蔚竭_(dá)同步代碼塊時(shí),線程會(huì)判斷此時(shí)持有鎖的線程是否就是自己(持有鎖的線程ID也在對(duì)象頭里),如果是則正常往下執(zhí)行。由于之前沒(méi)有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個(gè),很明顯偏向鎖幾乎沒(méi)有額外開銷,性能極高。
偏向鎖是指當(dāng)一段同步代碼一直被同一個(gè)線程所訪問(wèn)時(shí),即不存在多個(gè)線程的競(jìng)爭(zhēng)時(shí),那么該線程在后續(xù)訪問(wèn)時(shí)便會(huì)自動(dòng)獲得鎖,從而降低獲取鎖帶來(lái)的消耗,即提高性能。
當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí),會(huì)在 Mark Word 里存儲(chǔ)鎖偏向的線程 ID。在線程進(jìn)入和退出同步塊時(shí)不再通過(guò) CAS 操作來(lái)加鎖和解鎖,而是檢測(cè) Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。輕量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令即可。
偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程是不會(huì)主動(dòng)釋放偏向鎖的。關(guān)于偏向鎖的撤銷,需要等待全局安全點(diǎn),即在某個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行時(shí),它會(huì)先暫停擁有偏向鎖的線程,然后判斷鎖對(duì)象是否處于被鎖定狀態(tài)。如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài),并撤銷偏向鎖,恢復(fù)到無(wú)鎖(標(biāo)志位為01)或輕量級(jí)鎖(標(biāo)志位為00)的狀態(tài)。
輕量級(jí)鎖
輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,卻被另外的線程所訪問(wèn),此時(shí)偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,線程不會(huì)阻塞,從而提高性能。
輕量級(jí)鎖的獲取主要由兩種情況:
一旦有第二個(gè)線程加入鎖競(jìng)爭(zhēng),偏向鎖就升級(jí)為輕量級(jí)鎖(自旋鎖)。這里要明確一下什么是鎖競(jìng)爭(zhēng):如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取鎖的時(shí)候都很順利,沒(méi)有發(fā)生阻塞,那么就不存在鎖競(jìng)爭(zhēng)。只有當(dāng)某線程嘗試獲取鎖的時(shí)候,發(fā)現(xiàn)該鎖已經(jīng)被占用,只能等待其釋放,這才發(fā)生了鎖競(jìng)爭(zhēng)。
在輕量級(jí)鎖狀態(tài)下繼續(xù)鎖競(jìng)爭(zhēng),沒(méi)有搶到鎖的線程將自旋,即不停地循環(huán)判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實(shí)就是通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位。先比較當(dāng)前鎖標(biāo)志位是否為“釋放”,如果是則將其設(shè)置為“鎖定”,比較并設(shè)置是原子性發(fā)生的。這就算搶到鎖了,然后線程將當(dāng)前鎖的持有者信息修改為自己。
長(zhǎng)時(shí)間的自旋操作是非常消耗資源的,一個(gè)線程持有鎖,其他線程就只能在原地空耗CPU,執(zhí)行不了任何有效的任務(wù),這種現(xiàn)象叫做忙等(busy-waiting)。如果多個(gè)線程用一個(gè)鎖,但是沒(méi)有發(fā)生鎖競(jìng)爭(zhēng),或者發(fā)生了很輕微的鎖競(jìng)爭(zhēng),那么synchronized就用輕量級(jí)鎖,允許短時(shí)間的忙等現(xiàn)象。這是一種折衷的想法,短時(shí)間的忙等,換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。
重量級(jí)鎖
重量級(jí)鎖顯然,此忙等是有限度的(有個(gè)計(jì)數(shù)器記錄自旋次數(shù),默認(rèn)允許循環(huán)10次,可以通過(guò)虛擬機(jī)參數(shù)更改)。如果鎖競(jìng)爭(zhēng)情況嚴(yán)重,某個(gè)達(dá)到最大自旋次數(shù)的線程,會(huì)將輕量級(jí)鎖升級(jí)為重量級(jí)鎖(依然是CAS修改鎖標(biāo)志位,但不修改持有鎖的線程ID)。當(dāng)后續(xù)線程嘗試獲取鎖時(shí),發(fā)現(xiàn)被占用的鎖是重量級(jí)鎖,則直接將自己掛起(而不是忙等),等待將來(lái)被喚醒。
重量級(jí)鎖是指當(dāng)有一個(gè)線程獲取鎖之后,其余所有等待獲取該鎖的線程都會(huì)處于阻塞狀態(tài)。簡(jiǎn)言之,就是所有的控制權(quán)都交給了操作系統(tǒng),由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度和線程的狀態(tài)變更。而這樣會(huì)出現(xiàn)頻繁地對(duì)線程運(yùn)行狀態(tài)的切換,線程的掛起和喚醒,從而消耗大量的系統(tǒng)資。
擴(kuò)展閱讀
synchronized 用的鎖是存在Java對(duì)象頭里的,那么什么是對(duì)象頭呢?我們以 Hotspot 虛擬機(jī)為例進(jìn)行說(shuō)明,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 Klass Pointer(類型指針)。
- Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無(wú)關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。
- Klass Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
那么,synchronized 具體是存在對(duì)象頭哪里呢?答案是:存在鎖對(duì)象的對(duì)象頭的Mark Word中,那么MarkWord在對(duì)象頭中到底長(zhǎng)什么樣,它到底存儲(chǔ)了什么呢?
在64位的虛擬機(jī)中:
在32位的虛擬機(jī)中:
下面我們以 32位虛擬機(jī)為例,來(lái)看一下其 Mark Word 的字節(jié)具體是如何分配的:
- 無(wú)鎖 :對(duì)象頭開辟 25bit 的空間用來(lái)存儲(chǔ)對(duì)象的 hashcode ,4bit 用于存放對(duì)象分代年齡,1bit 用來(lái)存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來(lái)存放鎖標(biāo)識(shí)位為01。
- 偏向鎖: 在偏向鎖中劃分更細(xì),還是開辟 25bit 的空間,其中23bit 用來(lái)存放線程ID,2bit 用來(lái)存放 Epoch,4bit 存放對(duì)象分代年齡,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無(wú)鎖,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01。
- 輕量級(jí)鎖:在輕量級(jí)鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00。
- 重量級(jí)鎖: 在重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來(lái)存放指向重量級(jí)鎖的指針,2bit 存放鎖的標(biāo)識(shí)位,為11。
- GC標(biāo)記: 開辟30bit 的內(nèi)存空間卻沒(méi)有占用,2bit 空間存放鎖標(biāo)志位為11。
其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)。關(guān)于內(nèi)存的分配,我們可以在git中openJDK中 markOop.hpp 可以看出:
public:// Constantsenum { age_bits = 4,lock_bits = 2,biased_lock_bits = 1,max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,cms_bits = LP64_ONLY(1) NOT_LP64(0),epoch_bits = 2};- age_bits: 就是我們說(shuō)的分代回收的標(biāo)識(shí),占用4字節(jié)。
- lock_bits: 是鎖的標(biāo)志位,占用2個(gè)字節(jié)。
- biased_lock_bits: 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)。
- max_hash_bits: 是針對(duì)無(wú)鎖計(jì)算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte。
- hash_bits: 是針對(duì) 64 位虛擬機(jī)來(lái)說(shuō),如果最大字節(jié)數(shù)大于 31,則取31,否則取真實(shí)的字節(jié)數(shù)。
- cms_bits: 不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte。
- epoch_bits: 就是 epoch 所占用的字節(jié)大小,2字節(jié)。
4.21 如何實(shí)現(xiàn)互斥鎖(mutex)?
參考答案
在Java里面,最基本的互斥同步手段就是synchronized關(guān)鍵字,這是一種塊結(jié)構(gòu)(Block Structured)的同步語(yǔ)法。synchronized關(guān)鍵字經(jīng)過(guò)Javac編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這兩個(gè)字節(jié)碼指令。這兩個(gè)字節(jié)碼指令都需要一個(gè)reference類型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象。如果Java源碼中的synchronized明確指定了對(duì)象參數(shù),那就以這個(gè)對(duì)象的引用作為reference。如果沒(méi)有明確指定,那將根據(jù)synchronized修飾的方法類型(如實(shí)例方法或類方法),來(lái)決定是取代碼所在的對(duì)象實(shí)例還是取類型對(duì)應(yīng)的Class對(duì)象來(lái)作為線程要持有的鎖。
自JDK 5起,Java類庫(kù)中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一種全新的互斥同步手段。基于Lock接口,用戶能夠以非塊結(jié)構(gòu)(Non-Block Structured)來(lái)實(shí)現(xiàn)互斥同步,從而擺脫了語(yǔ)言特性的束縛,改為在類庫(kù)層面去實(shí)現(xiàn)同步,這也為日后擴(kuò)展出不同調(diào)度算法、不同特征、不同性能、不同語(yǔ)義的各種鎖提供了廣闊的空間。
4.22 分段鎖是怎么實(shí)現(xiàn)的?
參考答案
在并發(fā)程序中,串行操作是會(huì)降低可伸縮性,并且上下文切換也會(huì)減低性能。在鎖上發(fā)生競(jìng)爭(zhēng)時(shí)將通水導(dǎo)致這兩種問(wèn)題,使用獨(dú)占鎖時(shí)保護(hù)受限資源的時(shí)候,基本上是采用串行方式—-每次只能有一個(gè)線程能訪問(wèn)它。所以對(duì)于可伸縮性來(lái)說(shuō)最大的威脅就是獨(dú)占鎖。
我們一般有三種方式降低鎖的競(jìng)爭(zhēng)程度:
在某些情況下我們可以將鎖分解技術(shù)進(jìn)一步擴(kuò)展為一組獨(dú)立對(duì)象上的鎖進(jìn)行分解,這稱為分段鎖。其實(shí)說(shuō)的簡(jiǎn)單一點(diǎn)就是:容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而可以有效的提高并發(fā)訪問(wèn)效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn)。
如下圖,ConcurrentHashMap使用Segment數(shù)據(jù)結(jié)構(gòu),將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn),能夠?qū)崿F(xiàn)真正的并發(fā)訪問(wèn)。所以說(shuō),ConcurrentHashMap在并發(fā)情況下,不僅保證了線程安全,而且提高了性能。
4.23 說(shuō)說(shuō)你對(duì)讀寫鎖的了解
參考答案
與傳統(tǒng)鎖不同的是讀寫鎖的規(guī)則是可以共享讀,但只能一個(gè)寫,總結(jié)起來(lái)為:讀讀不互斥、讀寫互斥、寫寫互斥,而一般的獨(dú)占鎖是:讀讀互斥、讀寫互斥、寫寫互斥,而場(chǎng)景中往往讀遠(yuǎn)遠(yuǎn)大于寫,讀寫鎖就是為了這種優(yōu)化而創(chuàng)建出來(lái)的一種機(jī)制。
注意是讀遠(yuǎn)遠(yuǎn)大于寫,一般情況下獨(dú)占鎖的效率低來(lái)源于高并發(fā)下對(duì)臨界區(qū)的激烈競(jìng)爭(zhēng)導(dǎo)致線程上下文切換。因此當(dāng)并發(fā)不是很高的情況下,讀寫鎖由于需要額外維護(hù)讀鎖的狀態(tài),可能還不如獨(dú)占鎖的效率高。因此需要根據(jù)實(shí)際情況選擇使用。
在Java中ReadWriteLock的主要實(shí)現(xiàn)為ReentrantReadWriteLock,其提供了以下特性:
4.24 volatile關(guān)鍵字有什么用?
參考答案
當(dāng)一個(gè)變量被定義成volatile之后,它將具備兩項(xiàng)特性:
保證可見性
當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去,這個(gè)寫會(huì)操作會(huì)導(dǎo)致其他線程中的volatile變量緩存無(wú)效。
禁止指令重排
使用volatile關(guān)鍵字修飾共享變量可以禁止指令重排序,volatile禁止指令重排序有一些規(guī)則:
- 當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見,在其后面的操作肯定還沒(méi)有進(jìn)行;
- 在進(jìn)行指令優(yōu)化時(shí),不能將對(duì)volatile變量訪問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把volatile變量后面的語(yǔ)句放到其前面執(zhí)行。
即執(zhí)行到volatile變量時(shí),其前面的所有語(yǔ)句都執(zhí)行完,后面所有語(yǔ)句都未執(zhí)行。且前面語(yǔ)句的結(jié)果對(duì)volatile變量及其后面語(yǔ)句可見。
注意,雖然volatile能夠保證可見性,但它不能保證原子性。volatile變量在各個(gè)線程的工作內(nèi)存中是不存在一致性問(wèn)題的,但是Java里面的運(yùn)算操作符并非原子操作,這導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。
4.25 談?wù)剉olatile的實(shí)現(xiàn)原理
參考答案
volatile可以保證線程可見性且提供了一定的有序性,但是無(wú)法保證原子性。在JVM底層volatile是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的。觀察加入volatile關(guān)鍵字和沒(méi)有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令,lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障,內(nèi)存屏障會(huì)提供3個(gè)功能:
4.26 說(shuō)說(shuō)你對(duì)JUC的了解
參考答案
JUC是java.util.concurrent的縮寫,該包參考自EDU.oswego.cs.dl.util.concurrent,是JSR 166標(biāo)準(zhǔn)規(guī)范的一個(gè)實(shí)現(xiàn)。JSR 166是一個(gè)關(guān)于Java并發(fā)編程的規(guī)范提案,在JDK中該規(guī)范由java.util.concurrent包實(shí)現(xiàn)。即JUC是Java提供的并發(fā)包,其中包含了一些并發(fā)編程用到的基礎(chǔ)組件。
JUC這個(gè)包下的類基本上包含了我們?cè)诓l(fā)編程時(shí)用到的一些工具,大致可以分為以下幾類:
-
原子更新
Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環(huán) 境下,無(wú)鎖的進(jìn)行原子操作。在Atomic包里一共有12個(gè)類,四種原子更新方式,分別是原子更新基本類型,原子更新 數(shù)組,原子更新引用和原子更新字段。
-
鎖和條件變量
java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS構(gòu)建的Lock以及與Lock配合可以實(shí)現(xiàn)等待/通知模式的Condition。JUC 下的大多數(shù)工具類用到了Lock和Condition來(lái)實(shí)現(xiàn)并發(fā)。
-
線程池
涉及到的類比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。
-
阻塞隊(duì)列
涉及到的類比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。
-
并發(fā)容器
涉及到的類比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。
-
同步器
剩下的是一些在并發(fā)編程中時(shí)常會(huì)用到的工具類,主要用來(lái)協(xié)助線程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。
4.27 說(shuō)說(shuō)你對(duì)AQS的理解
參考答案
抽象隊(duì)列同步器AbstractQueuedSynchronizer (以下都簡(jiǎn)稱AQS),是用來(lái)構(gòu)建鎖或者其他同步組件的骨架類,減少了各功能組件實(shí)現(xiàn)的代碼量,也解決了在實(shí)現(xiàn)同步器時(shí)涉及的大量細(xì)節(jié)問(wèn)題,例如等待線程采用FIFO隊(duì)列操作的順序。在不同的同步器中還可以定義一些靈活的標(biāo)準(zhǔn)來(lái)判斷某個(gè)線程是應(yīng)該通過(guò)還是等待。
AQS采用模板方法模式,在內(nèi)部維護(hù)了n多的模板的方法的基礎(chǔ)上,子類只需要實(shí)現(xiàn)特定的幾個(gè)方法(不是抽象方法!不是抽象方法!不是抽象方法!),就可以實(shí)現(xiàn)子類自己的需求。
基于AQS實(shí)現(xiàn)的組件,諸如:
- ReentrantLock 可重入鎖(支持公平和非公平的方式獲取鎖);
- Semaphore 計(jì)數(shù)信號(hào)量;
- ReentrantReadWriteLock 讀寫鎖。
擴(kuò)展閱讀
AQS內(nèi)部維護(hù)了一個(gè)int成員變量來(lái)表示同步狀態(tài),通過(guò)內(nèi)置的FIFO(first-in-first-out)同步隊(duì)列來(lái)控制獲取共享資源的線程。
我們可以猜測(cè)出,AQS其實(shí)主要做了這么幾件事情:
- 同步狀態(tài)(state)的維護(hù)管理;
- 等待隊(duì)列的維護(hù)管理;
- 線程的阻塞與喚醒。
通過(guò)AQS內(nèi)部維護(hù)的int型的state,可以用于表示任意狀態(tài)!
- ReentrantLock用它來(lái)表示鎖的持有者線程已經(jīng)重復(fù)獲取該鎖的次數(shù),而對(duì)于非鎖的持有者線程來(lái)說(shuō),如果state大于0,意味著無(wú)法獲取該鎖,將該線程包裝為Node,加入到同步等待隊(duì)列里。
- Semaphore用它來(lái)表示剩余的許可數(shù)量,當(dāng)許可數(shù)量為0時(shí),對(duì)未獲取到許可但正在努力嘗試獲取許可的線程來(lái)說(shuō),會(huì)進(jìn)入同步等待隊(duì)列,阻塞,直到一些線程釋放掉持有的許可(state+1),然后爭(zhēng)用釋放掉的許可。
- FutureTask用它來(lái)表示任務(wù)的狀態(tài)(未開始、運(yùn)行中、完成、取消)。
- ReentrantReadWriteLock在使用時(shí),稍微有些不同,int型state用二進(jìn)制表示是32位,前16位(高位)表示為讀鎖,后面的16位(低位)表示為寫鎖。
- CountDownLatch使用state表示計(jì)數(shù)次數(shù),state大于0,表示需要加入到同步等待隊(duì)列并阻塞,直到state等于0,才會(huì)逐一喚醒等待隊(duì)列里的線程。
AQS通過(guò)內(nèi)置的FIFO(first-in-first-out)同步隊(duì)列來(lái)控制獲取共享資源的線程。CLH隊(duì)列是FIFO的雙端雙向隊(duì)列,AQS的同步機(jī)制就是依靠這個(gè)CLH隊(duì)列完成的。隊(duì)列的每個(gè)節(jié)點(diǎn),都有前驅(qū)節(jié)點(diǎn)指針和后繼節(jié)點(diǎn)指針。如下圖:
4.28 LongAdder解決了什么問(wèn)題,它是如何實(shí)現(xiàn)的?
參考答案
高并發(fā)下計(jì)數(shù),一般最先想到的應(yīng)該是AtomicLong/AtomicInt,AtmoicXXX使用硬件級(jí)別的指令 CAS 來(lái)更新計(jì)數(shù)器的值,這樣可以避免加鎖,機(jī)器直接支持的指令,效率也很高。但是AtomicXXX中的 CAS 操作在出現(xiàn)線程競(jìng)爭(zhēng)時(shí),失敗的線程會(huì)白白地循環(huán)一次,在并發(fā)很大的情況下,因?yàn)槊看蜟AS都只有一個(gè)線程能成功,競(jìng)爭(zhēng)失敗的線程會(huì)非常多。失敗次數(shù)越多,循環(huán)次數(shù)就越多,很多線程的CAS操作越來(lái)越接近 自旋鎖(spin lock)。計(jì)數(shù)操作本來(lái)是一個(gè)很簡(jiǎn)單的操作,實(shí)際需要耗費(fèi)的cpu時(shí)間應(yīng)該是越少越好,AtomicXXX在高并發(fā)計(jì)數(shù)時(shí),大量的cpu時(shí)間都浪費(fèi)會(huì)在 自旋 上了,這很浪費(fèi),也降低了實(shí)際的計(jì)數(shù)效率。
LongAdder是jdk8新增的用于并發(fā)環(huán)境的計(jì)數(shù)器,目的是為了在高并發(fā)情況下,代替AtomicLong/AtomicInt,成為一個(gè)用于高并發(fā)情況下的高效的通用計(jì)數(shù)器。說(shuō)LongAdder比在高并發(fā)時(shí)比AtomicLong更高效,這么說(shuō)有什么依據(jù)呢?LongAdder是根據(jù)鎖分段來(lái)實(shí)現(xiàn)的,它里面維護(hù)一組按需分配的計(jì)數(shù)單元,并發(fā)計(jì)數(shù)時(shí),不同的線程可以在不同的計(jì)數(shù)單元上進(jìn)行計(jì)數(shù),這樣減少了線程競(jìng)爭(zhēng),提高了并發(fā)效率。本質(zhì)上是用空間換時(shí)間的思想,不過(guò)在實(shí)際高并發(fā)情況中消耗的空間可以忽略不計(jì)。
現(xiàn)在,在處理高并發(fā)計(jì)數(shù)時(shí),應(yīng)該優(yōu)先使用LongAdder,而不是繼續(xù)使用AtomicLong。當(dāng)然,線程競(jìng)爭(zhēng)很低的情況下進(jìn)行計(jì)數(shù),使用Atomic還是更簡(jiǎn)單更直接,并且效率稍微高一些。其他情況,比如序號(hào)生成,這種情況下需要準(zhǔn)確的數(shù)值,全局唯一的AtomicLong才是正確的選擇,此時(shí)不應(yīng)該使用LongAdder。
4.29 介紹下ThreadLocal和它的應(yīng)用場(chǎng)景
參考答案
ThreadLocal顧名思義是線程私有的局部變量存儲(chǔ)容器,可以理解成每個(gè)線程都有自己專屬的存儲(chǔ)容器,它用來(lái)存儲(chǔ)線程私有變量,其實(shí)它只是一個(gè)外殼,內(nèi)部真正存取是一個(gè)Map。每個(gè)線程可以通過(guò)set()和get()存取變量,多線程間無(wú)法訪問(wèn)各自的局部變量,相當(dāng)于在每個(gè)線程間建立了一個(gè)隔板。只要線程處于活動(dòng)狀態(tài),它所對(duì)應(yīng)的ThreadLocal實(shí)例就是可訪問(wèn)的,線程被終止后,它的所有實(shí)例將被垃圾收集。總之記住一句話:ThreadLocal存儲(chǔ)的變量屬于當(dāng)前線程。
ThreadLocal經(jīng)典的使用場(chǎng)景是為每個(gè)線程分配一個(gè) JDBC 連接 Connection,這樣就可以保證每個(gè)線程的都在各自的 Connection 上進(jìn)行數(shù)據(jù)庫(kù)的操作,不會(huì)出現(xiàn) A 線程關(guān)了 B線程正在使用的 Connection。 另外ThreadLocal還經(jīng)常用于管理Session會(huì)話,將Session保存在ThreadLocal中,使線程處理多次處理會(huì)話時(shí)始終是同一個(gè)Session。
4.30 請(qǐng)介紹ThreadLocal的實(shí)現(xiàn)原理,它是怎么處理hash沖突的?
參考答案
Thread類中有個(gè)變量threadLocals,它的類型為ThreadLocal中的一個(gè)內(nèi)部類ThreadLocalMap,這個(gè)類沒(méi)有實(shí)現(xiàn)map接口,就是一個(gè)普通的Java類,但是實(shí)現(xiàn)的類似map的功能。每個(gè)線程都有自己的一個(gè)map,map是一個(gè)數(shù)組的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù),每個(gè)元素是一個(gè)Entry,entry的key是ThreadLocal的引用,也就是當(dāng)前變量的副本,value就是set的值。代碼如下所示:
public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null; }ThreadLocalMap是ThreadLocal的內(nèi)部類,每個(gè)數(shù)據(jù)用Entry保存,其中的Entry繼承與WeakReference,用一個(gè)鍵值對(duì)存儲(chǔ),鍵為ThreadLocal的引用。為什么是WeakReference呢?如果是強(qiáng)引用,即使把ThreadLocal設(shè)置為null,GC也不會(huì)回收,因?yàn)門hreadLocalMap對(duì)它有強(qiáng)引用。代碼如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} }ThreadLocal中的set方法的實(shí)現(xiàn)邏輯,先獲取當(dāng)前線程,取出當(dāng)前線程的ThreadLocalMap,如果不存在就會(huì)創(chuàng)建一個(gè)ThreadLocalMap,如果存在就會(huì)把當(dāng)前的threadlocal的引用作為鍵,傳入的參數(shù)作為值存入map中。代碼如下所示:
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);} }ThreadLocal中g(shù)et方法的實(shí)現(xiàn)邏輯,獲取當(dāng)前線程,取出當(dāng)前線程的ThreadLocalMap,用當(dāng)前的threadlocak作為key在ThreadLocalMap查找,如果存在不為空的Entry,就返回Entry中的value,否則就會(huì)執(zhí)行初始化并返回默認(rèn)的值。代碼如下所示:
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue(); }ThreadLocal中remove方法的實(shí)現(xiàn)邏輯,還是先獲取當(dāng)前線程的ThreadLocalMap變量,如果存在就調(diào)用ThreadLocalMap的remove方法。ThreadLocalMap的存儲(chǔ)就是數(shù)組的實(shí)現(xiàn),因此需要確定元素的位置,找到Entry,把entry的鍵值對(duì)都設(shè)為null,最后也Entry也設(shè)置為null。其實(shí)這其中會(huì)有哈希沖突,具體見下文。代碼如下所示:
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);} }ThreadLocal中的hash code非常簡(jiǎn)單,就是調(diào)用AtomicInteger的getAndAdd方法,參數(shù)是個(gè)固定值0x61c88647。上面說(shuō)過(guò)ThreadLocalMap的結(jié)構(gòu)非常簡(jiǎn)單只用一個(gè)數(shù)組存儲(chǔ),并沒(méi)有鏈表結(jié)構(gòu),當(dāng)出現(xiàn)Hash沖突時(shí)采用線性查找的方式,所謂線性查找,就是根據(jù)初始key的hashcode值確定元素在table數(shù)組中的位置,如果發(fā)現(xiàn)這個(gè)位置上已經(jīng)有其他key值的元素被占用,則利用固定的算法尋找一定步長(zhǎng)的下個(gè)位置,依次判斷,直至找到能夠存放的位置。如果產(chǎn)生多次hash沖突,處理起來(lái)就沒(méi)有HashMap的效率高,為了避免哈希沖突,使用盡量少的threadlocal變量。
4.31 介紹一下線程池
參考答案
系統(tǒng)啟動(dòng)一個(gè)新線程的成本是比較高的,因?yàn)樗婕芭c操作系統(tǒng)交互。在這種情形下,使用線程池可以很好地提高性能,尤其是當(dāng)程序中需要?jiǎng)?chuàng)建大量生存期很短暫的線程時(shí),更應(yīng)該考慮使用線程池。
與數(shù)據(jù)庫(kù)連接池類似的是,線程池在系統(tǒng)啟動(dòng)時(shí)即創(chuàng)建大量空閑的線程,程序?qū)⒁粋€(gè)Runnable對(duì)象或Callable對(duì)象傳給線程池,線程池就會(huì)啟動(dòng)一個(gè)空閑的線程來(lái)執(zhí)行它們的run()或call()方法,當(dāng)run()或call()方法執(zhí)行結(jié)束后,該線程并不會(huì)死亡,而是再次返回線程池中成為空閑狀態(tài),等待執(zhí)行下一個(gè)Runnable對(duì)象的run()或call()方法。
從Java 5開始,Java內(nèi)建支持線程池。Java 5新增了一個(gè)Executors工廠類來(lái)產(chǎn)生線程池,該工廠類包含如下幾個(gè)靜態(tài)工廠方法來(lái)創(chuàng)建線程池。創(chuàng)建出來(lái)的線程池,都是通過(guò)ThreadPoolExecutor類來(lái)實(shí)現(xiàn)的。
- newCachedThreadPool():創(chuàng)建一個(gè)具有緩存功能的線程池,系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建線程,這些線程將會(huì)被緩存在線程池中。
- newFixedThreadPool(int nThreads):創(chuàng)建一個(gè)可重用的、具有固定線程數(shù)的線程池。
- newSingleThreadExecutor():創(chuàng)建一個(gè)只有單線程的線程池,它相當(dāng)于調(diào)用newFixedThread Pool()方法時(shí)傳入?yún)?shù)為1。
- newScheduledThreadPool(int corePoolSize):創(chuàng)建具有指定線程數(shù)的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。corePoolSize指池中所保存的線程數(shù),即使線程是空閑的也被保存在線程池內(nèi)。
- newSingleThreadScheduledExecutor():創(chuàng)建只有一個(gè)線程的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。
- ExecutorService newWorkStealingPool(int parallelism):創(chuàng)建持有足夠的線程的線程池來(lái)支持給定的并行級(jí)別,該方法還會(huì)使用多個(gè)隊(duì)列來(lái)減少競(jìng)爭(zhēng)。
- ExecutorService newWorkStealingPool():該方法是前一個(gè)方法的簡(jiǎn)化版本。如果當(dāng)前機(jī)器有4個(gè)CPU,則目標(biāo)并行級(jí)別被設(shè)置為4,也就是相當(dāng)于為前一個(gè)方法傳入4作為參數(shù)。
4.32 介紹一下線程池的工作流程
參考答案
線程池的工作流程如下圖所示:
4.33 線程池都有哪些狀態(tài)?
參考答案
線程池一共有五種狀態(tài), 分別是:
- 線程池不是RUNNING狀態(tài);
- 線程池狀態(tài)不是TIDYING狀態(tài)或TERMINATED狀態(tài);
- 如果線程池狀態(tài)是SHUTDOWN并且workerQueue為空;
- workerCount為0;
- 設(shè)置TIDYING狀態(tài)成功。
下圖為線程池的狀態(tài)轉(zhuǎn)換過(guò)程:
4.34 談?wù)劸€程池的拒絕策略
參考答案
當(dāng)線程池的任務(wù)緩存隊(duì)列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來(lái)就會(huì)采取任務(wù)拒絕策略,通常有以下四種策略:
4.35 線程池的隊(duì)列大小你通常怎么設(shè)置?
參考答案
CPU密集型任務(wù)
盡量使用較小的線程池,一般為CPU核心數(shù)+1。 因?yàn)镃PU密集型任務(wù)使得CPU使用率很高,若開過(guò)多的線程數(shù),會(huì)造成CPU過(guò)度切換。
IO密集型任務(wù)
可以使用稍大的線程池,一般為2*CPU核心數(shù)。 IO密集型任務(wù)CPU使用率并不高,因此可以讓CPU在等待IO的時(shí)候有其他線程去處理別的任務(wù),充分利用CPU時(shí)間。
混合型任務(wù)
可以將任務(wù)分成IO密集型和CPU密集型任務(wù),然后分別用不同的線程池去處理。 只要分完之后兩個(gè)任務(wù)的執(zhí)行時(shí)間相差不大,那么就會(huì)比串行執(zhí)行來(lái)的高效。因?yàn)槿绻麆澐种髢蓚€(gè)任務(wù)執(zhí)行時(shí)間有數(shù)據(jù)級(jí)的差距,那么拆分沒(méi)有意義。因?yàn)橄葓?zhí)行完的任務(wù)就要等后執(zhí)行完的任務(wù),最終的時(shí)間仍然取決于后執(zhí)行完的任務(wù),而且還要加上任務(wù)拆分與合并的開銷,得不償失。
4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?
參考答案
線程池主要有如下6個(gè)參數(shù):
corePoolSize(核心工作線程數(shù)):當(dāng)向線程池提交一個(gè)任務(wù)時(shí),若線程池已創(chuàng)建的線程數(shù)小于corePoolSize,即便此時(shí)存在空閑線程,也會(huì)通過(guò)創(chuàng)建一個(gè)新線程來(lái)執(zhí)行該任務(wù),直到已創(chuàng)建的線程數(shù)大于或等于corePoolSize時(shí)。
maximumPoolSize(最大線程數(shù)):線程池所允許的最大線程個(gè)數(shù)。當(dāng)隊(duì)列滿了,且已創(chuàng)建的線程數(shù)小于maximumPoolSize,則線程池會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)。另外,對(duì)于無(wú)界隊(duì)列,可忽略該參數(shù)。
keepAliveTime(多余線程存活時(shí)間):當(dāng)線程池中線程數(shù)大于核心線程數(shù)時(shí),線程的空閑時(shí)間如果超過(guò)線程存活時(shí)間,那么這個(gè)線程就會(huì)被銷毀,直到線程池中的線程數(shù)小于等于核心線程數(shù)。
workQueue(隊(duì)列):用于傳輸和保存等待執(zhí)行任務(wù)的阻塞隊(duì)列。
threadFactory(線程創(chuàng)建工廠):用于創(chuàng)建新線程。threadFactory創(chuàng)建的線程也是采用new Thread()方式,threadFactory創(chuàng)建的線程名都具有統(tǒng)一的風(fēng)格:pool-m-thread-n(m為線程池的編號(hào),n為線程池內(nèi)的線程編號(hào))。
數(shù)。 IO密集型任務(wù)CPU使用率并不高,因此可以讓CPU在等待IO的時(shí)候有其他線程去處理別的任務(wù),充分利用CPU時(shí)間。
混合型任務(wù)
可以將任務(wù)分成IO密集型和CPU密集型任務(wù),然后分別用不同的線程池去處理。 只要分完之后兩個(gè)任務(wù)的執(zhí)行時(shí)間相差不大,那么就會(huì)比串行執(zhí)行來(lái)的高效。因?yàn)槿绻麆澐种髢蓚€(gè)任務(wù)執(zhí)行時(shí)間有數(shù)據(jù)級(jí)的差距,那么拆分沒(méi)有意義。因?yàn)橄葓?zhí)行完的任務(wù)就要等后執(zhí)行完的任務(wù),最終的時(shí)間仍然取決于后執(zhí)行完的任務(wù),而且還要加上任務(wù)拆分與合并的開銷,得不償失。
4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?
參考答案
線程池主要有如下6個(gè)參數(shù):
總結(jié)
以上是生活随笔為你收集整理的【2022】多线程并发编程面试真题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 套接字(socket)基本知识与工作原理
- 下一篇: Backtrader交易基础2