阿里三面:Java的synchronized 能防止指令重排序嗎?

java金融發表於2022-04-16

引言

二狗:二胖你昨天請假了是不是又去面試了啊?
二胖:別說了我就出去試試水,看看現在工作好不好找,順帶出去找找打擊,然後才能好好靜下心來好好學習。
二狗: 那被打擊的怎麼樣啊?知道自己是什麼樣的水平了吧,壞笑。
二胖:基礎太差,一面就讓回去等通知了,我要好好學習了,不跟你瞎扯了。
二狗: 都問了你什麼問題啊,把你打擊成這樣?一起復盤下讓我也好好準備下啊。
二胖:好吧,你既然這麼好奇,那我就大概說下吧,你搬上小板凳仔細挺好了哦。我要開始我的表演了。
下面二胖第一面開始了。
面試官:二胖是吧,先做個自我介紹吧。
二胖:好的,我叫二胖,我來自長沙,今年25歲,從事java開發快3年了,現在在XX公司XX事業部擔任高階java開發工程師,主要負責XX系統。。。。。
面試官:好的,我看你簡歷上寫著熟練掌握併發程式設計你能跟我說說併發程式設計裡面你都知道哪些關鍵字。
二胖: 這不就是要考我 synchronizedvolatile 這個我擅長啊,我特意背過的,synchronizedjava提供的一個關鍵字它主要能保證原子性、有序性它的底層主要是通過Monitor來實現的。volatile也是java的一個關鍵字它的主要作用是可以保證可見性。。。。此處省略1000字。
面試官:八股文背的不錯,說了這麼多,我們來動手試試吧,寫一個雙重校驗鎖(dcl)的單例我看看。
二胖: 從屁股口袋裡拿出了筆三下五除二就把它默寫出來了。
面試官:你有說道volatile關鍵字和synchronized關鍵字。synchronized可以保證原子性、有序性和可見性。而volatile卻只能保證有序性和可見性。那麼,我們再來看一下雙重校驗鎖實現的單例,已經使用了synchronized,為什麼還需要volatile?這個volatile是否可以去掉?
二胖: 讓我想想,貌似好像確實可以去掉。
面試官: 我們今天的面試就到這裡吧,後續有訊息人事會聯絡你,感謝你今天來面試。

二胖很鬱悶回去谷歌了下這個問題,stackoverflow上也有這個問題,看樣子不只我一個人不知道這個問題嗎?看樣子面試掛的不冤
以上故事純屬虛構,如有雷同請以本文為主。

synchronized 的有序性?

我們先來看看沒有加volatile 修飾的單例:

 1   public class Singleton {  
 2      private static Singleton singleton;  
 3       private Singleton (){}  
 4       public static Singleton getSingleton() {  
 5       if (singleton == null) {  
 6           synchronized (Singleton.class) {  
 7               if (singleton == null) {  
 8                   singleton = new Singleton();  
 9               }  
 10           }  
 11       }  
 12       return singleton;  
 13       }  
 14   }  

上述程式碼看下來是不是感覺沒啥問題。
首先我們先來看下這一行程式碼到底幹了哪些事情

singleton = new Singleton() 

來源網上
上述過程我們可以簡化成3個步驟:

  • JVM為物件分配一塊記憶體M。
  • ②在記憶體M上為物件進行初始化。
  • ③將記憶體M的地址複製給singleton變數。
    這個步驟有兩種執行順序可以按照 ①②③或者①③②來執行。當我們按照①③②的順序來執行的時候
    我們假設有兩個執行緒ThreadAThreadB 同時來請求Singleton.getSingleton方法:
  • 正常情況按照 ①②③的順序來執行
    第一步:ThreadA 進入到第8行,執行 singleton = new Singleton() 進行物件的初始化(按照物件初始化的過程 ①②③)執行完。
    第二步: ThreadB進入第5行判斷singleton不為空(第一步已經初始化好了),直接返回singleton
    第三步:拿到這個物件做其他的操作。
    這樣看下來是不是沒有啥問題。
  • 那如果物件初始化的時候按照 ①③② 的步驟我們再來看看:
    第一步: ThreadA進入到第8行,執行 singleton = new Singleton() 執行完.①JVM為物件分配一塊記憶體M。③將記憶體的地址複製給singleton變數。
    第二步: 此時ThreadB直接進入第5行,發現singleton已經不為空了然後直接就跳轉到12行拿到這個singleton返回去執行操作去了。此時ThreadB拿到的singleton物件是個半成品物件,因為還沒有為這個物件進行初始化(②還沒執行)。
    第三步: 所以ThreadB拿到的物件去執行方法可能會有異常產生。至於為什麼會這樣列?《Java 併發程式設計實戰》有提到

    有 synchronized 無 volatile 的 DCL(雙重檢查鎖) 會出現的情況:執行緒可能看到引用的當前值,但物件的狀態值確少失效的,這意味著執行緒可以看到物件處於無效或錯誤的狀態。

說白了也就是ThreadB是可以拿到一個引用已經有了但是記憶體資源還沒有分配的物件。
如果要解決建立物件按照①②③的順序,其實也就是為了解決指令重排只要第2行加個volatile修飾就好。
說好的synchronized 不是可以保證有序性的嗎?volatile的有序性?synchronized 不能不夠保證指令重排嗎?
怎麼來定義順序呢?《深入理解Java虛擬機器第三版》有提到

Java程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有操作都是天然有序的。如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。前半句是指“執行緒內似表現為序列的語義”,後半句是指“指令重排”現象和“工作記憶體與主記憶體同步延遲”現象。
  • synchronized 的有序性是持有相同鎖的兩個同步塊只能序列的進入,即被加鎖的內容要按照順序被多個執行緒執行,但是其內部的同步程式碼還是會發生重排序,使塊與塊之間有序可見。
  • volatile的有序性是通過插入記憶體屏障來保證指令按照順序執行。不會存在後面的指令跑到前面的指令之前來執行。是保證編譯器優化的時候不會讓指令亂序。
  • synchronized 是不能保證指令重排的

**本文參與了 SegmentFault 思否徵文「如何“反殺”面試官?」,歡迎正在閱讀的你也加入。
**

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。

站在巨人的肩膀上摘蘋果:
https://stackoverflow.com/que...
https://juejin.cn/post/684490...

相關文章