指令重排序和記憶體屏障
一、指令重排序
指令重排序分為三種,分別為編譯器優化重排序、指令級並行重排序、記憶體系統重排序。如圖所示,後面兩種為處理器級別(即為硬體層面)。
- 編譯器優化重排序:編譯器在不改變程式執行結果的情況下,為了提升效率,對指令進行亂序的編譯。例如在程式碼中A操作需要獲取其他資源而進入等待的狀態,而A操作後面的程式碼跟其沒有依賴關係,如果編譯器一直等待A操作完成再往下執行的話效率要慢的多,所以可以先編譯後面的程式碼,這樣的亂序可以提升不小的編譯速度。
- 指令級並行重排序:處理器在不影響程式執行結果的情況下,將多條指令重疊在一起執行,同樣也是為了提升效率。
- 記憶體系統重排序:這個跟之前兩個不同的是,其為偽重排序,也就是說只是看起來像在亂序執行而已。對於現代的處理器來說,在CPU和主記憶體之間都具備一個快取記憶體,快取記憶體的作用主要為減少CPU和主記憶體的互動(CPU的處理速度要快的多),在CPU進行讀操作時,如果快取沒有的話從主記憶體取,而對於寫操作都是先寫在快取中,最後再一次性寫入主記憶體,原因是減少跟主記憶體互動時CPU的短暫卡頓,從而提升效能,但是延時寫入可能會導致一個問題——資料不一致。
// CPU1執行以下操作 a = 1; int i = b; // CPU2執行下面操作 b = 1; int j = a;
其執行圖如下:從上面圖中我們可以看到,對於CPU來說,先將a = 1寫入快取在讀取變數b,過後在寫入a到主記憶體,而這個操作從表面上看就變成了先讀取變數b,在寫入a到主記憶體,也就是發生了重排序,所以才說這為偽重排序。
而從上面我們也可以看出,由於CPU1和2寫入的時機不同,最終可能導致讀到的(a,b)變數有四種情況,分別是(0,0),(0,1),(1,0),(1,1)。例如,在兩個快取未寫入主記憶體的時候就進行變數讀取,這時候讀到的就為(0,0),其他情況類推。所以Java在實現記憶體模型的時候會禁止特定型別的重排序。
as-if-serial語義:這是重排序都需要遵循的規則,其大致意思就是在單執行緒中,只要不改變程式的最終執行結果,那麼為了提升效能可以改變指令執行的順序。
二、記憶體屏障
在編譯器方面使用volatile關鍵字可以禁止指令重排序,而在硬體方面實現禁止指令重排序的則是記憶體屏障。其中包括硬體層本來就有的LoadBarriers和StoreBarriers 和JVM封裝實現的四種記憶體屏障。
從硬體層上
記憶體屏障分為兩種,LoadBarriers和StoreBarriers。
-
- LoadBarriers:在執行屏障後一個操作前,保證已經重新整理了快取的資料,也就是說使快取失效,強制從記憶體重新整理資料到快取中。
i = a; LoadBarriers; // ..其他操作
如上虛擬碼中,在執行其他操作之前必須保證a的變數從主記憶體中讀取並且重新整理到快取中。
- StoreBarriers:此屏障之前的寫入快取中的資料同步到記憶體中,並且保證其他執行緒可見。
a = 1; b = 2; c = 3; StoreBarriers; // ..其他操作
如上虛擬碼中,保證在其他操作之前,寫入快取中的a,b,c三個變數同步到主記憶體中,並且其他執行緒可以觀察到變數的變化。
- LoadBarriers:在執行屏障後一個操作前,保證已經重新整理了快取的資料,也就是說使快取失效,強制從記憶體重新整理資料到快取中。
JVM實現的記憶體屏障
- LoadLoad:對於Load1;LoadLoad;Load2這樣的情況,保證Load1先於Load2及之後的Load操作,且對其可見。例如:
... int i = a; LoadLoad; int j = b;
在這段程式碼中,在int j = b以及後面的Load操作中,都能見到int i = a的操作,也就是int i = a先於後面的讀取操作。即,禁止int i = a和之後的讀操作重排序。
- LoadStore:對於Load1;LoadStore;Store1來說,保證Load1操作先於Store1以及後面的Store操作,即對後Store操作可見。如:
int i = a; LoadStore b = 1;
// int i = a對於b = 1及之後的store操作均可見。 - StoreLoad:同上,Store1;StoreLoad;Load1情況來說,保證Store1操作先於後續的所有Load操作,並且其Store的變數操作對其他處理器可見。由於Store操作會立即重新整理到記憶體並對其他處理器快取可見的特性,其具備其他三個屏障的功能,但是相對的,其花費的開銷較大。
- StoreStore:在Store1;StoreStore;Store2情況中,保證Store1操作先於Store2操作,即在Store1後續的Store操作之前,Store1操作保證重新整理到記憶體並且對其他處理器可見。
volatile的禁止指令重排序
我們都知道volatile關鍵字有兩個語義:
-
- 保證記憶體可見性
- 禁止指令重排序
其中JVM對其禁止指令重排序在硬體層面的實現就是通過在volatile修飾的變數前後插入記憶體屏障。volatile變數的記憶體屏障規則如下:
在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;
在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;
而在編譯器方面則是因為對於volatile變數記憶體中的六種操作會有特殊的規則,可以看看我的另一篇文章——淺談記憶體模型,裡面介紹了volatile兩種語義的原理,同時也說明了volatile關鍵字沒有原子性的原因。
文章若有不正之處,還望指出,在此多謝!