天天看點

并發程式設計相關面試題一

并發程式設計相關面試題一

一、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來保證有序性,它們能保證那一塊區域裡的代碼都是一次性執行完畢的。

posted on

2020-03-27 15:24 

關耳er 

閱讀(...) 

評論(...) 

編輯 

收藏