前言
對(duì)Android開(kāi)發(fā)者來(lái)說(shuō),相信對(duì)并發(fā)編程知識(shí)的掌握是非常薄弱的,一直是個(gè)人進(jìn)階的軟肋之一。對(duì)于并發(fā)實(shí)踐經(jīng)驗(yàn)缺乏的開(kāi)發(fā)者來(lái)說(shuō),文縐縐的技術(shù)書(shū)籍和博客,會(huì)比較羞澀難懂。從本文開(kāi)始,嘗試著逐個(gè)攻破并發(fā)編程的基礎(chǔ)知識(shí)點(diǎn)。
由于無(wú)知與惰性,讓我們感覺(jué)摸到了技術(shù)的天花板!
面試10問(wèn)
本文結(jié)合個(gè)人實(shí)際面試經(jīng)驗(yàn)和最近學(xué)習(xí)歸納總結(jié)而出,歡迎各位大佬點(diǎn)贊支持。
通過(guò)面試10問(wèn),讓大家掌握單例模式的雙重檢查模式和靜態(tài)內(nèi)部類單例模式,并了解其中原理。從原理進(jìn)而引出本文的重點(diǎn):volatile和synchronized。
第1問(wèn):平常在Android開(kāi)發(fā)中,有用到哪么設(shè)計(jì)模式么?
當(dāng)時(shí)回答:平常用的比較多的是單例模式、構(gòu)造者模式、工廠模式。尤其是單例模式中雙重檢查模式和靜態(tài)類單例模式;能夠保證多線程對(duì)象唯一,不會(huì)創(chuàng)建多個(gè)實(shí)例導(dǎo)致程序執(zhí)行錯(cuò)誤或影響性能。
解讀:雖然設(shè)計(jì)模式有很多種,個(gè)人來(lái)說(shuō),經(jīng)常用也就單例模式了。雖然面試前突擊瀏覽復(fù)習(xí)了,然面試一緊張,沒(méi)啥卵用。所以回答一定要往自己了解的說(shuō),并引導(dǎo)面試官往自己會(huì)的問(wèn)。
第2問(wèn):在紙上寫(xiě)一下雙重檢查模式和靜態(tài)類單例模式代碼?
心理活動(dòng):還好面試前自己已經(jīng)默寫(xiě)過(guò)很多遍了,問(wèn)題不大,嘩啦啦的寫(xiě)出來(lái):
雙重檢查模式:
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
復(fù)制代碼
靜態(tài)內(nèi)部類模式:
public class Singleton {
private Singleton(){
}
public static Singleton getSingleton(){
return Inner.instance;
}
private static class Inner {
private static final Singleton instance = new Singleton();
}
}
復(fù)制代碼
寫(xiě)好遞給面試官:雙重檢查模式和單例模式都能夠有效保證線程安全,又都是延時(shí)初始化,能夠減少不必要的性能開(kāi)銷。
第3問(wèn):雙重檢查模式有什么需要注意地方?
public class Singleton {
private volatile static Singleton singleton;
private Singleton() { } //1
public static Singleton getSingleton() { //2
if (singleton == null) { // 3.1
synchronized (Singleton.class) { //3.2
if (singleton == null) { //3.3
singleton = new Singleton(); //4
}
}
}
return singleton;
}
}
復(fù)制代碼
答:雙重檢查模式需要注意以下幾點(diǎn):
- 構(gòu)造函數(shù)得私有,禁止其他對(duì)象直接創(chuàng)建實(shí)例;
- 對(duì)外提供一個(gè)靜態(tài)方法,可以獲取唯一的實(shí)例;
- 即然是雙重檢查模式,就意味著創(chuàng)建實(shí)例過(guò)程會(huì)有兩層檢查。第一層就是最外層的判空語(yǔ)句:
代碼3.1處的if (singleton == null),該判斷沒(méi)有加鎖處理,避免第一次檢查singleton對(duì)象非null時(shí),多線程加鎖和初始化操作;當(dāng)前對(duì)象未創(chuàng)建時(shí),通過(guò)synchronized關(guān)鍵字同步代碼塊,持有當(dāng)前Singleton.class的鎖,保證線程安全,然后進(jìn)行第二次檢查。 -
Singleton類持有的singleton實(shí)例引用需要volatile關(guān)鍵字修飾,因?yàn)樵谧詈笠徊?code>singleton = new Singleton(); 創(chuàng)建實(shí)例的時(shí)候可能會(huì)重排序,導(dǎo)致singleton對(duì)象逸出,導(dǎo)致其他線程獲取到一個(gè)未初始化完畢的對(duì)象。
第4問(wèn):剛剛講到的第四點(diǎn),為什么會(huì)有重排序,volatile關(guān)鍵字如何禁止重排序?
答:重排序是指編輯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重排序的一種手段。只要遵守as -if-serial語(yǔ)義(無(wú)論怎么重排序,單線程程序的執(zhí)行結(jié)果不會(huì)改變)。所以編譯器為了優(yōu)化性能,可能會(huì)對(duì)下圖中2和3步驟進(jìn)行重排序,這種重排序時(shí)允許的,因?yàn)椴粫?huì)改變單線程(目前只有該線程獨(dú)占該代碼塊)內(nèi)程序的執(zhí)行結(jié)果。
volatile修飾的變量本身具有可見(jiàn)性和原子性,所謂的可見(jiàn)性是指對(duì)一個(gè)volatile變量的讀值,讀到的值是所有線程中最新修改的值;而原子性是指對(duì)單個(gè)變量的讀寫(xiě)具有原子性。之所以會(huì)有這兩個(gè)特性,是因?yàn)闀?huì)在該共享變量的匯編指令之前增加Lock指令,該Lock前綴指令會(huì)在多核處理器做兩件事:
1、將當(dāng)前處理器緩存行的數(shù)據(jù)寫(xiě)回到系統(tǒng)內(nèi)存;
2、這個(gè)寫(xiě)回內(nèi)存的操作會(huì)使其他處理器里緩存了該內(nèi)存地址的數(shù)據(jù)無(wú)效。
ps:?jiǎn)魏颂幚砥饕粫r(shí)刻只能有一條線程執(zhí)行,多線程是指單核CPU對(duì)不同線程進(jìn)行上下文切換和調(diào)度;多核處理器同一個(gè)時(shí)刻可能多條線程(每個(gè)核一條線程)并發(fā)執(zhí)行, 這時(shí)同步非常重要,現(xiàn)代CPU基本都是多核了。
由于volatie變量的可見(jiàn)性這個(gè)特性使其 寫(xiě)-讀 建立起了happens-before關(guān)系,從內(nèi)存語(yǔ)義的角度上說(shuō),線程A寫(xiě)一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來(lái)將要讀這個(gè)volatiel變量的某個(gè)線程發(fā)出了通知。原理上講的話,在寫(xiě)一個(gè)volatile變量是,JAVA內(nèi)存模型(JMM)會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存;而在讀volatile變量時(shí),會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效,從主內(nèi)存中讀取該變量。線程之間通過(guò)共享程序的volatile變量(共享狀態(tài)),通過(guò)寫(xiě)讀操作共享狀態(tài)進(jìn)行隱式通信。
JMM為了實(shí)現(xiàn)這種volatile內(nèi)存語(yǔ)義,會(huì)限制編譯器和處理器的部分重排序。
為編譯器優(yōu)化制定以下三條規(guī)則 :
- 第一個(gè)操作是對(duì)volatile變量的讀,無(wú)論第二個(gè)操作是什么,都禁止重排序;
- 第一個(gè)操作是對(duì)volatile變量的寫(xiě),第二個(gè)操作是對(duì)volatile的讀,禁止重排序;
- 第二個(gè)操作是對(duì)volatile變量的寫(xiě),無(wú)論第一個(gè)操作是什么,都禁止重排序;
從第2條規(guī)則就可以理解通過(guò)添加volatile關(guān)鍵字修飾單例的引用,可以禁止重排序。
根據(jù)這三條規(guī)則,編譯器會(huì)在生成字節(jié)碼時(shí),在指令序列插入適當(dāng)?shù)?,保守策略的?nèi)存屏障(一組CPU指令,實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制)。
- volatile寫(xiě)操作前插入StoreStore屏障;
- volatile寫(xiě)操作后插入StoreLoad屏障;
- volatile讀操作后插入LoadLoad屏障;
- volatile讀操作后插入LoadStore屏障;
以上內(nèi)存屏障時(shí)非常保守,編譯器在生成字節(jié)碼時(shí),也會(huì)進(jìn)行部分優(yōu)化,減少一些不必要的內(nèi)存屏障,以提高性能。不同的處理器會(huì)根據(jù)自身的內(nèi)存模型繼續(xù)優(yōu)化。
ps:JMM是為了屏蔽底層硬件內(nèi)存模型不一致,為頂層開(kāi)發(fā)提供一套標(biāo)準(zhǔn)的內(nèi)存模型,讓開(kāi)發(fā)這專注要業(yè)務(wù)開(kāi)發(fā)。
第5問(wèn):剛剛提到的happens-before規(guī)則,具體怎么說(shuō)來(lái)的?
答:從JDK5開(kāi)始,使用了新的JSR-133內(nèi)存模型,該模型定義了happens-before 規(guī)則:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程的任意后續(xù)操作;
- 監(jiān)視器原則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)該鎖的加鎖;
- volatile規(guī)則:對(duì)一個(gè)volatile變量的寫(xiě),happens-before 于任意后續(xù)對(duì)這個(gè)volatile域的讀;
- 傳遞性:如果A happes-before B,B happens-before C,那么A happens-before C;
- start()原則:線程A執(zhí)行ThreadB.start()操作,start() happens-before 線程B內(nèi)所有操作;
- jion()原則:如果線程A執(zhí)行 ThreadB.jion()并成功返回,那線程B的所有操作都happens-before 于A從jion()操作成功返回。
第6問(wèn):規(guī)則第2點(diǎn)講到了鎖,那鎖在雙重檢查單例模式起了什么作用?
答:在代碼3.2處,用到了synchronized 關(guān)鍵字,對(duì)Singletion.Class對(duì)象進(jìn)行了同步,確保了在多線程環(huán)境下只有一個(gè)線程對(duì)Singletion類的Class對(duì)象進(jìn)行實(shí)例化。在Java中,每一個(gè)對(duì)象都可以作為鎖:
- 對(duì)于普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象;
- 對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的Class對(duì)象;
- 對(duì)于同步方法塊,鎖是Synchoized括號(hào)的Class對(duì)象。
第7問(wèn):靜態(tài)內(nèi)部類單例模式有沒(méi)有用到鎖?
答:有的,JVM在類的初始化階段(在Class被加載后,且在線程使用之前),會(huì)執(zhí)行類的初始化,JVM會(huì)去獲取一個(gè)鎖,這個(gè)鎖能同步多個(gè)線程對(duì)同一個(gè)類的初始化。
當(dāng)一個(gè)線程A獲取到這個(gè)初始化鎖時(shí),其他線程想要獲取初始化鎖只能等待;線程A執(zhí)行類靜態(tài)初始化和初始化靜態(tài)字段的過(guò)程,就算發(fā)生類似雙重檢查模式的重排序,對(duì)結(jié)果也沒(méi)有影響,因?yàn)榇藭r(shí)沒(méi)有其他線程可以捕獲到初始化鎖。線程A初始化完畢,釋放鎖并通知等待獲取初始化鎖的線程。根據(jù)happens-befroe關(guān)系中的監(jiān)視器規(guī)則,當(dāng)其他線程獲取到初始鎖時(shí),已經(jīng)能看到線程A的初始化所有操作,此時(shí)靜態(tài)對(duì)象已經(jīng)初始化完畢,其他線程無(wú)需再初始化。
第8問(wèn):了解過(guò)鎖的原理,知道鎖存儲(chǔ)在哪么?
答:JVM(Java虛擬機(jī))是基于進(jìn)入和退出Monitor對(duì)象來(lái)實(shí)現(xiàn)方法同步和代碼塊同步的。同步代碼塊使用monitorenter指令在編譯后插入到同步代碼塊的開(kāi)始位置,使用monitorexit插入到同步代碼塊的結(jié)束處或異常處,monitorenter必須有對(duì)應(yīng)monitorexit指令與之配對(duì)。任何對(duì)象都有一個(gè)monitor與之相關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有后,將處于鎖定狀態(tài)。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),即獲得對(duì)象的鎖。方法則是在方法的指令前增加ACC_SYNCHRONIZED修飾符。
Synchronized用的鎖是存放在Java的對(duì)象頭;如果對(duì)象是數(shù)組,用3字寬存儲(chǔ)對(duì)象頭,其中一字寬用于存儲(chǔ)數(shù)組長(zhǎng)度;非數(shù)組,則2字寬存儲(chǔ)對(duì)象頭。在32位虛擬機(jī),1字寬=4字節(jié)=32位。

第9問(wèn):即然了解過(guò)Java的對(duì)象頭,那應(yīng)該清楚鎖升級(jí)的幾種狀態(tài)吧,說(shuō)一下?
答:在Java SE6,為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,引入了偏向鎖和輕量級(jí)鎖。意味著此時(shí)鎖從低到高共有四種狀態(tài):無(wú)鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)。鎖的狀態(tài)是根據(jù)線程對(duì)鎖的競(jìng)爭(zhēng)情況來(lái)定義的。32位JVM運(yùn)行狀態(tài)下,Mark Work的存儲(chǔ)結(jié)構(gòu):

偏向鎖: 線程在大多數(shù)情況下并不存在競(jìng)爭(zhēng)條件,使用同步會(huì)消耗性能,而偏向鎖是對(duì)鎖的優(yōu)化,可以消除同步,提升性能。當(dāng)一個(gè)線程獲得鎖,會(huì)將對(duì)象頭的鎖標(biāo)志位設(shè)為01,進(jìn)入偏向模式.偏向鎖可以在讓一個(gè)線程一直持有鎖,在其他線程需要競(jìng)爭(zhēng)鎖的時(shí)候,再釋放鎖。==》只有一個(gè)線程進(jìn)入臨界區(qū)。
輕量級(jí)鎖: 當(dāng)線程A獲得偏向鎖后,線程B進(jìn)入競(jìng)爭(zhēng)狀態(tài),需要獲得線程A持有的鎖,那么線程A撤銷偏向鎖,進(jìn)入無(wú)鎖狀態(tài)。線程A和線程B交替進(jìn)入臨界區(qū),偏向鎖無(wú)法滿足,膨脹到輕量級(jí)鎖,鎖標(biāo)志位設(shè)為00。==》多個(gè)線程交替進(jìn)入臨界區(qū)。
重量級(jí)鎖: 當(dāng)多線程交替進(jìn)入臨界區(qū),輕量級(jí)鎖hold得住。但如果多個(gè)線程同時(shí)進(jìn)入臨界區(qū),hold不住了,膨脹到重量級(jí)鎖==》多個(gè)線程同時(shí)進(jìn)入臨界區(qū)。
第10問(wèn):為什么Synchronized夠用,還要增加Volatile?
Volatile相對(duì)Synchronized來(lái)說(shuō)在同步上比較輕量級(jí),能夠有效降低CPU頻繁的線程上下文切換和調(diào)度。同時(shí),Volatile的原子操作是針對(duì)單個(gè)volatile變量的寫(xiě)讀操作,無(wú)法和Sychronized對(duì)整個(gè)方法或代碼塊起的作用相比較。
總結(jié)
基本每一問(wèn)都會(huì)涉及到一些知識(shí)點(diǎn),面試官也會(huì)從不同方向去提問(wèn),引出不同知識(shí)點(diǎn)。例如后面幾個(gè)問(wèn)題可以引出Java的內(nèi)存模型,這些都是面試的高頻問(wèn)題。
通過(guò)本文,需要掌握單例模式的兩種寫(xiě)法:Java:單例模式我只推薦兩種。還需掌握volatile和synchronized的知識(shí)點(diǎn)。
由于個(gè)人能力有限,有錯(cuò)誤歡迎指正;或者覺(jué)得方向不好,歡迎指導(dǎo),非常感謝。
最后希望:
點(diǎn)贊&關(guān)注=true