標籤:style blog http io ar 使用 java for sp
指令重排序
對主存的一次訪問一般花費硬體的數百次刻度。處理器通過緩衝(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法則如下:
1, 程式次序法則,如果A一定在B之前發生,則happen before,
2, 監視器法則,對一個監視器的解鎖一定發生在後續對同一監視器加鎖之前
3, Volatie變數法則:寫volatile變數一定發生在後續對它的讀之前
4, 線程啟動法則:Thread.start一定發生線上程中的動作
5, 線程終結法則:線程中的任何動作一定發生在括弧中的動作之前(其他線程檢測到這個線程已經終止,從Thread.join調用成功返回,Thread.isAlive()返回false)
6, 中斷法則:一個線程調用另一個線程的interrupt一定發生在另一線程發現中斷。
7, 終結法則:一個對象的建構函式結束一定發生在對象的finalizer之前
8, 傳遞性:A發生在B之前,B發生在C之前,A一定發生在C之前。
轉自:http://blog.163.com/javaee_chen/blog/static/179195077201131382128499/
指令重排序及Happens-before法則隨筆