JAVA并發編程之驗證volatile指令重排-理論篇
Java并發包下的類中大量使用了volatile關鍵字。通過之前文章介紹,大家已經知道了volatile的三大特性:共享變量可見性;不保證原子性;禁止指令重排后順序性。通過前面兩篇文章我們通過代碼驗證了前兩個特性,本文我們就來驗證禁止指令重排保證順序性。
指令重排序的生活例子
去餐廳吃飯預定位置的的時候。假設要去A餐廳吃飯,A餐廳有前臺B、服務員C以及老板D。如果就只有你一個人去吃飯的時候,你給前臺或者給服務器或者給老板說一聲把2號桌預定了,半小時后過來。餐廳在為了2小時內就你一個人去吃飯。那么OK,沒問題,別說等半個小時,就是等一個小時,2號桌還是你的。
但是,如果現在是吃飯高峰期,很多人來吃飯,你給前臺說了,前臺忙著沒有及時給服務員或者沒有給老板說,這個時候有個路人甲來吃飯,剛好看到2號桌沒人,老板或者服務員就讓他就坐2號桌吃飯了。那么,等你過來的時候,2號桌已經有人了。這個時候對于你來說,這個結果就不是你想要的了。
上面案例,如果從計算機執行指令角度來分析的話,你要到2號桌吃飯,這是預期結果。餐廳A就相當于是處理器,前臺B就相當于是編譯器,服務員C和老板D就是指令和內存系統。如果你預定的時間點不是吃飯高峰期或者沒有人去餐廳A吃飯。那么你就相當于是一個線程。就是單線程的。老板、前臺、服務員怎么安排都可以。因為只有你一個2號桌肯定是你的。這是單線程情況下。預期結果與實際結果就是一致的。
如果你預定的時間點是吃飯高峰期,很多人來吃飯(很多線程),這個時候為了餐廳效益,無論是前臺還是服務員或者是老板都會對你的位置進行重排序。在你沒有來的時候,會安排其他人到你預定的位置吃飯。如果其他人在你的位置吃飯,這個時候你再來吃飯,那么實際結果和預期結果就不一樣了。這個時候餐廳應該做出相應的賠償。為了解決這種賠償問題,老板就想到了一個方案。做個牌子放在客人預定的桌子上。
當前臺或者是服務員或者是老板看到餐桌上放的這個牌子,就知道這個位置不能再調動了。其中這個放在餐桌上的牌子就是特殊類型的內存屏障了。
示意圖如下:
再來舉個更常見的例子:
考試,在考試的時候老師會告訴我們,先做會做的,不會做的放到后面做。假設出題老師出題順序是1-5,但是考試會根據自己實際情況做題順序有可能是1、2、4、5、3或者是1、3、4、5、2等等。如果把出題老師看著是寫代碼的程序員,題目的順序是代碼一行一行的順序,你的老師會告訴你先做會做的,此時老師就相當于是編譯器,會排序一次。然后你自己做的時候又會進行重新排序,你自己就相當于是處理器又排序了一次。
上面兩個現實生活中的案例,我們弄明白后,再來看看在計算機中指令重排問題,就很容易理解了。
指令重排
我們程序員編寫的代碼在JVM執行的時候,為了提高性能,編譯器和處理器都會對代碼編譯后的指令進行重排序。分為3種:
1:編譯器優化重排:
編譯器的優化前提是在保證不改變單線程語義的情況下,對重新安排語句的執行順序。
2:指令并行重排:
如果代碼中某些語句之間不存在數據依賴,處理器可以改變語句對應機器指令的順序
如:int x = 10;int y = 5;對于這種x y之間沒有數據依賴關系的,機器指令就會進行重新排序。但是對于:int x = 10; int y = 5; int z = x+y;這種的,因為z和x y之間存在數據依賴(z=x+y)關系。在這種情況下,機器指令就不會把z排序在xy前面。
3:內存系統的重排序
通過之前的學習,我們知道了處理器和主內存之間還存在一二三級緩存。這些讀寫緩存的存在,使得程序的加載和存取操作,可能是亂序無章的。
指令重排序的流程圖
通過上面介紹,我們可以知道從程序員寫的Java源碼到處理器真正實際執行的指令序列,會經歷如下圖的過程:
執行順序:
源碼編譯器優化重排序(第一次排序) 指令重排序(第二次)內存重排序(第三次) 最終指向的指令。
無論是第一次編譯器的重排序還是第二、三次的處理器重排序。這些重排序當在多線程的場景下可能會出現線程可見性的問題。
如在多線程的情況下,單例模式就不安全了。
為了解決這個問題,JMM允許編譯器在生成指令順序的時候,可以插入特定類型的內存屏障來禁止指令重排序。
當一個變量使用volatile修飾的時候,volatile關鍵字就是內存屏障。當編譯器在生成指令順序的時候,發現了volatile,就直接忽略掉。不再重排序了。
示意圖: