併發程式設計相關面試題一
一、volatile
1、volatile的應用
在多執行緒併發程式中synchronized和volatile都扮演者著很重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的可見性,能夠防止髒讀,被volatile關鍵字修飾的變數,如果值發生了改變,其他執行緒立刻可見;
可見性的意思是當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值,如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低;
2、volatile定義
java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應該確保通過排他鎖單獨獲得這個變數;java語言提供了volatile,在某些情況下比鎖更加方便,如果一個欄位被宣告成volatile,java執行緒記憶體模型確保所有執行緒看到這個變數的值是一致的;
3、volatile和synchronized有什麼區別
volatile能夠保證資料可見性,但是無法保證資料操作的原子性;
synchronized能夠保證資料可見性,也能保證資料操作的原子性;
4、volatile使用條件
只能在有限的一些情形下使用volatile變數替代鎖;要使volatile變數提供理想的執行緒安全,必須滿足下面兩個條件:
①對變數的寫操作不依賴於當前值;
②該變數沒有包含在具體變數的不變式中;
實際上,這些條件宣告,可以被寫入volatile變數的這些有效值獨立於任何程式的狀態,包含變數的當前狀態;
第一個條件的限制使volatile變數不能用作執行緒安全計數器;雖然增量操作(i++)看上去類似於一個單獨的操作,實際上它是一個由(讀取-修改-寫入)操作序列組成的組合操作;必須以原子方式執行,而volatile不能提供必須的原子特性;實現正確的操作需要使i的值在操作期間保持不變,而volatile變數無法實現這點;
5、volatile優點
①記憶體中只有一個物件,減少記憶體開銷;
②單例可避免對資源的多重佔用,例如寫檔案工作,可避免對同一資原始檔的同時寫操作;
6、volatile缺點
①單例模式一般沒有介面,擴充套件很困難;
②不利於測試,並行開發時,若單例未完成,則不能進行測試;
③與單一職責原則衝突;
二、指令重排序
1、什麼是指令重排序
java語言規範JVM執行緒內部維持順序化語義,即只要程式的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以與程式碼邏輯不一致,這個過程就叫做執行重排序;
在執行程式時,為了提高效能,編譯器和處理器常常會對指令做重排序,重排序分為三種類型:
①編譯器優化的重排序:編譯器在不改變單執行緒程式的語義下,可以重新安排語句的執行順序;
②指令級並行重排序:執行緒處理器採用了指令級並行技術來將多條指令重疊執行;如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序;
③記憶體系統重排序:由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂執行;
最終執行的指令序列示意圖:
上述的1屬於編譯器重排序,2和3屬於處理器重排序;這些重排序可能會導致多執行緒程式出現記憶體可見性問題;對於編譯器,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序;對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別處理器重排序;
2、記憶體屏障
為了保證可見性,java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定型別的處理器重排序;
記憶體屏障型別表:
StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他三個屏障的效果;
三、先行發生原則Happens-before
從JDK1.5,java使用新的JSR-133記憶體模型;JSR-133使用happens-before的概念來闡述操作之間的記憶體可見性;
在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼者兩個操作之間必須要存在happens-before關係;這裡兩個操作可以是在一個執行緒之內,也可以是在不同執行緒之間;
happens-before八大原則
1.程式次序原則:
在一個執行緒內,按照程式碼的順序,書寫在前面的程式碼優先於書寫後面的程式碼;
2.管程鎖定規則:
一個unlock操作先行發生於後面對同一個鎖的lock操作,注意是同一個鎖;
3.volatile原則:
對於一個volatile變數的寫操作先行發生於後面對變數的讀操作;
4.執行緒啟動原則:
Thread物件的start()方法優先於此執行緒的每一個動作;
5.執行緒終止原則:
執行緒中所有的操作都優先發生於此執行緒的每一個動作;
6.物件中斷原則:
物件的interrupt()方法的呼叫優先發生於被中斷執行緒的程式碼監測中斷事件的發生;先中斷再檢測;
7.物件終結原則:
一個物件的初始化(建構函式執行完畢)完成優先發生於它的finalize()方法的開始;
8.傳遞性
如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
四、執行緒安全的三要素
1、原子性(Atomicity)
原子,一個不可再被分割的顆粒。原子性,指的是一個或多個不能再被分割的操作。
int i = 1; // 原子操作 i++; // 非原子操作,從主記憶體讀取 i 到執行緒工作記憶體,進行 +1,再把 i 寫到主記憶體。
雖然讀取和寫入都是原子操作,但合起來就不屬於原子操作,我們又叫這種為“複合操作”。
我們可以用synchronized 或 Lock 來把這個複合操作“變成”原子操作。
2、可見性(Visibility)
Java就是利用volatile來提供可見性的。
當一個變數被volatile修飾時,那麼對它的修改會立刻重新整理到主存,當其它執行緒需要讀取該變數時,會去記憶體中讀取新值。而普通變數則不能保證這一點。
其實通過synchronized和Lock也能夠保證可見性,執行緒在釋放鎖之前,會把共享變數值都刷回主存,但是synchronized和Lock的開銷都更大。
3、有序性(Ordering)
lock/unlock, volatile關鍵字可以產生記憶體屏障,防止指令重排序時越過
JMM是允許編譯器和處理器對指令重排序的,但是規定了as-if-serial語義,即不管怎麼重排序,程式的執行結果不能改變。比如下面的程式段:
double pi = 3.14; //A double r = 1; //B double s= pi * r * r; //C
上面的語句,可以按照A->B->C
執行,結果為3.14,但是也可以按照B->A->C
的順序執行,因為A、B是兩句獨立的語句,而C則依賴於A、B,所以A、B可以重排序,但是C卻不能排到A、B的前面。JMM保證了重排序不會影響到單執行緒的執行,但是在多執行緒中卻容易出問題。
如圖所示,write方法裡的1和2做了重排序,執行緒1先對flag賦值為true,隨後執行到執行緒2,ret直接計算出結果,再到執行緒1,這時候a才賦值為2,很明顯遲了一步。
這時候可以為flag加上volatile關鍵字,禁止重排序,可以確保程式的“有序性”,也可以上重量級的synchronized和Lock來保證有序性,它們能保證那一塊區域裡的程式碼都是一次性執行完畢的。