Java的synchronized 能防止指令重排序嗎?
引言
「二狗」:二胖你昨天請假了是不是又去面試了啊?
「二胖」:別說了我就出去試試水,看看現(xiàn)在工作好不好找,順帶出去找找打擊,然后才能好好靜下心來好好學(xué)習(xí)。
「二狗:」 那被打擊的怎么樣啊?知道自己是什么樣的水平了吧,壞笑。
「二胖」:基礎(chǔ)太差,一面就讓回去等通知了,我要好好學(xué)習(xí)了,不跟你瞎扯了。
「二狗:」 都問了你什么問題啊,把你打擊成這樣?一起復(fù)盤下讓我也好好準(zhǔn)備下啊。
「二胖」:好吧,你既然這么好奇,那我就大概說下吧,你搬上小板凳仔細(xì)挺好了哦。我要開始我的表演了。
下面二胖第一面開始了。
「面試官」:二胖是吧,先做個自我介紹吧。
「二胖」:好的,我叫二胖,我來自長沙,今年25歲,從事java開發(fā)快3年了,現(xiàn)在在XX公司XX事業(yè)部擔(dān)任高級「java」開發(fā)工程師,主要負(fù)責(zé)XX系統(tǒng)。。。。。
「面試官」:好的,我看你簡歷上寫著熟練掌握并發(fā)編程你能跟我說說并發(fā)編程里面你都知道哪些關(guān)鍵字。
「二胖:」 這不就是要考我 synchronized 和volatile 這個我擅長啊,我特意背過的,synchronized 是java提供的一個關(guān)鍵字它主要能保證原子性、有序性它的底層主要是通過Monitor來實現(xiàn)的。volatile也是java的一個關(guān)鍵字它的主要作用是可以保證可見性。。。。此處省略1000字。
「面試官」:八股文背的不錯,說了這么多,我們來動手試試吧,寫一個雙重校驗鎖(dcl)的單例我看看。
「二胖:」 從屁股口袋里拿出了筆三下五除二就把它默寫出來了。
「面試官」:你有說道volatile關(guān)鍵字和synchronized關(guān)鍵字。synchronized可以保證原子性、有序性和可見性。而volatile卻只能保證有序性和可見性。那么,我們再來看一下雙重校驗鎖實現(xiàn)的單例,已經(jīng)使用了synchronized,為什么還需要volatile?這個volatile是否可以去掉?
「二胖:」 讓我想想,貌似好像確實可以去掉。
「面試官:」 我們今天的面試就到這里吧,后續(xù)有消息人事會聯(lián)系你,感謝你今天來面試。
二胖很郁悶回去谷歌了下這個問題,「stackoverflow」上也有這個問題,看樣子不只我一個人不知道這個問題嗎?看樣子面試掛的不冤「以上故事純屬虛構(gòu),如有雷同請以本文為主。」
synchronized 的有序性?
我們先來看看沒有加volatile 修飾的單例:
- public class Singleton {
- private static Singleton singleton;
- private Singleton (){}
- public static Singleton getSingleton() {
- if (singleton == null) {
- synchronized (Singleton.class) {
- if (singleton == null) {
- singleton = new Singleton();
- }
- }
- }
- return singleton;
- }
- }
上述代碼看下來是不是感覺沒啥問題。首先我們先來看下這一行代碼到底干了哪些事情
- singleton = new Singleton()
上述過程我們可以簡化成3個步驟:
①「JVM」為對象分配一塊內(nèi)存M。
②在內(nèi)存M上為對象進(jìn)行初始化。
③將內(nèi)存M的地址復(fù)制給singleton變量。
這個步驟有兩種執(zhí)行順序可以按照 「①②③」或者「①③②」來執(zhí)行。當(dāng)我們按照「①③②」的順序來執(zhí)行的時候 我們假設(shè)有兩個線程ThreadA 和ThreadB 同時來請求Singleton.getSingleton方法:
正常情況按照 「①②③」的順序來執(zhí)行
「第一步:」ThreadA 進(jìn)入到第8行,執(zhí)行 singleton = new Singleton() 進(jìn)行對象的初始化(按照對象初始化的過程 「①②③」)執(zhí)行完。
「第二步:」 ThreadB進(jìn)入第5行判斷singleton不為空(第一步已經(jīng)初始化好了),直接返回singleton**第三步:**拿到這個對象做其他的操作。這樣看下來是不是沒有啥問題。
那如果對象初始化的時候按照 「①③②」 的步驟我們再來看看:「第一步:」 ThreadA進(jìn)入到第8行,執(zhí)行 singleton = new Singleton() 執(zhí)行完.①JVM為對象分配一塊內(nèi)存M。③將內(nèi)存的地址復(fù)制給singleton變量。
「第二步:」 此時ThreadB直接進(jìn)入第5行,發(fā)現(xiàn)singleton已經(jīng)不為空了然后直接就跳轉(zhuǎn)到12行拿到這個singleton返回去執(zhí)行操作去了。此時ThreadB拿到的singleton對象是個半成品對象,因為還沒有為這個對象進(jìn)行初始化(「②還沒執(zhí)行」)。「第三步:」 所以ThreadB拿到的對象去執(zhí)行方法可能會有異常產(chǎn)生。至于為什么會這樣列?《Java 并發(fā)編程實戰(zhàn)》有提到
有 synchronized 無 volatile 的 DCL(雙重檢查鎖) 會出現(xiàn)的情況:線程可能看到引用的當(dāng)前值,但對象的狀態(tài)值確少失效的,這意味著線程可以看到對象處于無效或錯誤的狀態(tài)。
說白了也就是ThreadB是可以拿到一個引用已經(jīng)有了但是內(nèi)存資源還沒有分配的對象。如果要解決創(chuàng)建對象按照①②③的順序,其實也就是為了解決指令重排只要第2行加個volatile修飾就好。
「說好的synchronized 不是可以保證有序性的嗎?volatile的有序性?synchronized 不能不夠保證指令重排嗎?」
怎么來定義順序呢?《深入理解Java虛擬機第三版》有提到
Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有操作都是天然有序的。如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內(nèi)似表現(xiàn)為串行的語義”,后半句是指“指令重排”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。
- 「synchronized」 的有序性是持有相同鎖的兩個同步塊只能串行的進(jìn)入,即被加鎖的內(nèi)容要按照順序被多個線程執(zhí)行,但是其內(nèi)部的同步代碼還是會發(fā)生重排序,使塊與塊之間有序可見。
- 「volatile」的有序性是通過插入內(nèi)存屏障來保證指令按照順序執(zhí)行。不會存在后面的指令跑到前面的指令之前來執(zhí)行。是保證編譯器優(yōu)化的時候不會讓指令亂序。
- 「synchronized 是不能保證指令重排的」。
結(jié)束
由于自己才疏學(xué)淺,難免會有紕漏,假如你發(fā)現(xiàn)了錯誤的地方,還望留言給我指出來,我會對其加以修正。
本文轉(zhuǎn)載自微信公眾號「 java金融」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 java金融公眾號。