程序員學習筆記:指令重排序及Happens-before法則

接指令重排序對主存的一次訪問通常花費硬件的數百次時鐘週期。處理器經過緩存(caching)可以從數量級上下降內存延遲的成本這些緩存爲了性能從新排列待定內存操做的順序。也就是說,程序的讀寫操做不必定會按照它要求處理器的順序執行。重排序的背景咱們知道現代CPU的主頻愈來愈高,與cache的交互次數也愈來愈多。當CPU的計算速度遠遠超過訪問cache時,會產生cache wait,過多的cache wait就會形成性能瓶頸。
針對這種狀況,多數架構(包括X86)採用了一種將cache分片的解決方案,即將一塊cache劃分紅互不關聯地多個 slots (邏輯存儲單元,又名 Memory Bank 或 Cache Bank),CPU能夠自行選擇在多個 idle bank 中進行存取。這種 SMP 的設計,顯著提升了CPU的並行處理能力,也迴避了cache訪問瓶頸。Memory Bank的劃分通常 Memory bank 是按cache address來劃分的。好比 偶數adress 0×12345000 分到 bank 0, 奇數address 0×12345100 分到 bank1。
重排序的種類編譯期重排。編譯源代碼時,編譯器依據對上下文的分析,對指令進行重排序,以之更適合於CPU的並行執行。運行期重排,CPU在執行過程當中,動態分析依賴部件的效能,對指令作重排序優化。Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。這個過程經過叫作指令的重排序。指令重排序存在的意義在於:JVM可以根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的從新排序機器指令,使機器指令更符合CPU的執行特色,最大限度的發揮機器的性能。程序執行最簡單的模型是按照指令出現的順序執行,這樣就與執行指令的CPU無關,最大限度的保證了指令的可移植性。這個模型的專業術語叫作順序化一致性模型。可是現代計算機體系和處理器架構都不保證這一點(由於人爲的指定並不能老是保證符合CPU處理的特性)。程序員


咱們來看最經典的一個案例:緩存

package xylz.study.concurrency.atomic; public class ReorderingDemo { static int x = 0, y = 0, a = 0, b = 0; public static void main(String[] args) throws Exception { for (int i = 0; i < 100; i++) {x=y=a=b=0;Thread one = new Thread() {public void run() {a = 1;x = b;}};Thread two = new Thread() {public void run() {b = 1;y = a;}};one.start();two.start();one.join();two.join();System.out.println(x + " " + y);}} }多線程

在這個例子中one/two兩個線程修改區x,y,a,b四個變量,在執行100次的狀況下,可能獲得(0 1)或者(1 0)或者(1 1)。事實上按照JVM的規範以及CPU的特性有極可能獲得(0 0)。固然上面的代碼你們不必定能獲得(0 0),由於run()裏面的操做過於簡單,可能比啓動一個線程花費的時間還少,所以上面的例子難以出現(0,0)。可是在現代CPU和JVM上確實是存在的。因爲run()裏面的動做對於結果是無關的,所以裏面的指令可能發生指令重排序,即便是按照程序的順序執行,數據變化刷新到主存也是須要時間的。
假定是按照a=1;x=b;b=1;y=a;執行的,x=0是比較正常的,雖然a=1在y=a以前執行的,可是因爲線程one執行a=1完成後尚未來得及將數據1寫回主存(這時候數據是在線程one的堆棧裏面的),線程two從主存中拿到的數據a可能仍然是0(顯然是一個過時數據,可是是有可能的),這樣就發生了數據錯誤。在兩個線程交替執行的狀況下數據的結果就不肯定了,在機器壓力大,多核CPU併發執行的狀況下,數據的結果就更加不肯定了。Happens-before法則Java的內存結構以下若是多線程之間不共享數據,這也表現得很好,可是若是多線程之間要共享數據,那麼這些亂序執行,數據在寄存器中這些行爲將致使程序行爲的不肯定性,如今處理器已是多核時代了,這些問題將會更加嚴重,每一個線程都有本身的工做內存,多個線程共享主內存,如圖若是共享數據,何時同步到主內存讓別人的線程讀取數據呢?這又是不肯定的,若是非要一致,那麼代價高昂,這將犧牲處理器的性能,因此如今的處理器會犧牲存儲一致性來換取性能,若是程序要確保共享數據的時候得到一致性,處理器一般了提供了一些關卡指令,這個能夠幫助程序員來實現,可是各類處理器都不同,若是要使程序可以跨平臺是不可能的,怎麼辦?
使用Java,由JMM(Java Memeory Model Action)來屏蔽,咱們只要和JMM的規定來使用一致性保證就搞定了,那麼JMM又提供了什麼保證呢?JMM的定義是經過動做的形式來描述的,所謂動做,包括變量的讀和寫,監視器加鎖和釋放鎖,線程的啓動和拼接,這就是傳說中的happen before,要想A動做看到B動做的結果,B和A必須知足happen before關係。最後爲你們總結了happen before法則:
一、程序次序法則,若是A必定在B以前發生,則happen before;
二、監視器法則,對一個監視器的解鎖必定發生在後續對同一監視器加鎖以前;
三、Volatie變量法則:寫volatile變量必定發生在後續對它的讀以前;
四、線程啓動法則:Thread.start必定發生在線程中的動做;
五、線程終結法則:線程中的任何動做必定發生在括號中的動做以前(其餘線程檢測到這個線程已經終止,從Thread.join調用成功返回,Thread.isAlive()返回false);
六、中斷法則:一個線程調用另外一個線程的interrupt必定發生在另外一線程發現中斷;
七、終結法則:一個對象的構造函數結束必定發生在對象的finalizer以前;
八、傳遞性:A發生在B以前,B發生在C以前,A必定發生在C以前。
 架構

相關文章
相關標籤/搜索