案例
public class MemoryReorderingExample {
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
while (true) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
}
}
}
}
結果
第多少多少次(0,0)
- 定義四個int類型的變量,初始化都為0。
- 定義兩個線程t1、t2
- t1線程修改a和x的值
- t2線程修改b和y的值
- 分別啟動兩個線程。
- 正常情況下,x和y的值,會根據t1和t2線程的執行情況來決定。
- 如果t1線程優先執行,那么得到的結果是x=0、y=1。
- 如果t2線程優先執行,那么得到的結果是x=1、y=0。
- 如果t1和t2線程同時執行,那么得到的結果是x=1、y=1。
為什么?結果為什么是 0 和 0。
其實這就是所謂的指令重排序問題,假設上面的代碼通過指令重排序之后,變成下面這種結構:
Thread t1=new Thread(()->{
x=b; //指令重排序
a=1;
});
Thread t2=new Thread(()->{
y=a; //指令重排序
b=1;
});
經過重排序之后,如果t1和t2線程同時運行,就會得到x=0、y=0的結果,這個結果從人的視角來看,就有點類似於t1線程中a=1的修改結果對t2線程不可見,同樣t2線程中b=1的執行結果對t1線程不可見。
什么是指令重排?
指令重排序是指編譯器或CPU為了優化程序的執行性能而對指令進行重新排序的一種手段,重排序會帶來可見性問題,所以在多線程開發中必須要關注並規避重排序。
從源代碼到最終運行的指令,會經過如下兩個階段的重排序。
第一階段,編譯器重排序,就是在編譯過程中,編譯器根據上下文分析對指令進行重排序,目的是減少CPU和內存的交互,重排序之后盡可能保證CPU從寄存器或緩存行中讀取數據。
在前面分析JIT優化中提到的循環表達式外提(Loop Expression Hoisting)就是編譯器層面的重排序,從CPU層面來說,避免了處理器每次都去內存中加載stop,減少了處理器和內存的交互開銷。
第二階段,處理器重排序,處理器重排序分為兩個部分。
- 並行指令集重排序,這是處理器優化的一種,處理器可以改變指令的執行順序。
- 內存系統重排序,這是處理器引入Store Buffer緩沖區延時寫入產生的指令執行順序不一致的問題。
擴展
什么是JIT?
1、動態編譯(dynamic compilation)指的是“在運行時進行編譯”;與之相對的是事前編譯(ahead-of-time compilation,簡稱AOT),也叫靜態編譯(static compilation)。
2、JIT 編譯(just-in-time compilation)狹義來說是當某段代碼即將第一次被執行時進行編譯,因而叫“即時編譯”。JIT編譯是動態編譯的一種特例。JIT編譯一詞后來被泛化,時常與動態編譯等價;但要注意廣義與狹義的JIT編譯所指的區別。
3、自適應動態編譯(adaptive dynamic compilation)也是一種動態編譯,但它通常執行的時機比JIT編譯遲,先讓程序“以某種式”先運行起來,收集一些信息之后再做動態編譯。這樣的編譯可以更加優化。

在部分商用虛擬機中(如HotSpot),Java程序最初是通過解釋器(Interpreter)進行解釋執行的,當虛擬機發現某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”。為了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平台相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler,下文統稱JIT編譯器)。
即時編譯器並不是虛擬機必須的部分,Java虛擬機規范並沒有規定Java虛擬機內必須要有即時編譯器存在,更沒有限定或指導即時編譯器應該如何去實現。但是,即時編譯器編譯性能的好壞、代碼優化程度的高低卻是衡量一款商用虛擬機優秀與否的最關鍵的指標之一,它也是虛擬機中最核心且最能體現虛擬機技術水平的部分。
由於Java虛擬機規范並沒有具體的約束規則去限制即使編譯器應該如何實現,所以這部分功能完全是與虛擬機具體實現相關的內容,如無特殊說明,我們提到的編譯器、即時編譯器都是指Hotspot虛擬機內的即時編譯器,虛擬機也是特指HotSpot虛擬機。
為什么HotSpot虛擬機要使用解釋器與編譯器並存的架構?
盡管並不是所有的Java虛擬機都采用解釋器與編譯器並存的架構,但許多主流的商用虛擬機(如HotSpot),都同時包含解釋器和編譯器。
解釋器與編譯器兩者各有優勢:當程序需要 迅速啟動和執行 的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行后,隨着時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取 更高的執行效率 。當程序運行環境中 內存資源限制較大 (如部分嵌入式系統中),可以使用 解釋器執行節約內存 ,反之可以使用 編譯執行來提升效率 。此外,如果編譯后出現“罕見陷阱”,可以通過逆優化退回到解釋執行。
HotSpot虛擬機中內置了兩個即時編譯器:Client Complier和Server Complier,簡稱為C1、C2編譯器,分別用在客戶端和服務端。目前主流的HotSpot虛擬機中默認是采用解釋器與其中一個編譯器直接配合的方式工作。程序使用哪個編譯器,取決於虛擬機運行的模式。HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用“-client”或“-server”參數去強制指定虛擬機運行在Client模式或Server模式。
用Client Complier獲取更高的編譯速度,用Server Complier 來獲取更好的編譯質量。為什么提供多個即時編譯器與為什么提供多個垃圾收集器類似,都是為了適應不同的應用場景。
編譯的時間開銷
解釋器的執行,抽象的看是這樣的:
*輸入的代碼 -> [ 解釋器 解釋執行 ] -> 執行結果
而要JIT編譯然后再執行的話,抽象的看則是:
*輸入的代碼 -> [ 編譯器 編譯 ] -> 編譯后的代碼 -> [ 執行 ] -> 執行結果
*說JIT比解釋快,其實說的是“執行編譯后的代碼”比“解釋器解釋執行”要快,並不是說“編譯”這個動作比“解釋”這個動作快。
JIT編譯再怎么快,至少也比解釋執行一次略慢一些,而要得到最后的執行結果還得再經過一個“執行編譯后的代碼”的過程。所以,對“只執行一次”的代碼而言,解釋執行其實總是比JIT編譯執行要快。
怎么算是“只執行一次的代碼”呢?粗略說,下面兩個條件同時滿足時就是嚴格的“只執行一次”
1、只被調用一次,例如類的構造器(class initializer,
2、沒有循環
對只執行一次的代碼做JIT編譯再執行,可以說是得不償失。
對只執行少量次數的代碼,JIT編譯帶來的執行速度的提升也未必能抵消掉最初編譯帶來的開銷。
只有對頻繁執行的代碼,JIT編譯才能保證有正面的收益。
什么是並行指令集?
在處理器內核中一般會有多個執行單元,比如算術邏輯單元、位移單元等。
- 在引入並行指令集之前,CPU在每個時鍾周期內只能執行單條指令,也就是說只有一個執行單元在工作,其他執行單元處於空閑狀態;
- 在引入並行指令集之后,CPU在一個時鍾周期內可以同時分配多條指令在不同的執行單元中執行。
那么什么是並行指令集的重排序呢?
如下圖所示,假設某一段程序有多條指令,不同指令的執行實現也不同。

對於一條從內存中讀取數據的指令,CPU的某個執行單元在執行這條指令並等到返回結果之前,按照CPU的執行速度來說它足夠處理幾百條其他指令,而CPU為了提高執行效率,會根據單元電路的空閑狀態和指令能否提前執行的情況進行分析,把那些指令地址順序靠后的指令提前到讀取內存指令之前完成。
實際上,這種優化的本質是通過提前執行其他可執行指令來填補CPU的時間空隙,然后在結束時重新排序運算結果,從而實現指令順序執行的運行結果。
as-if-serial語義
as-if-serial表示所有的程序指令都可以因為優化而被重排序,但是在優化的過程中必須要保證是在單線程環境下,重排序之后的運行結果和程序代碼本身預期的執行結果一致,Java編譯器、CPU指令重排序都需要保證在單線程環境下的as-if-serial語義是正確的。
可能有些讀者會有疑惑,既然能夠保證在單線程環境下的順序性,那為什么還會存在指令重排序呢?在JSR-133規范中,原文是這么說的。
The compiler, runtime, and hardware are supposed to conspire to create the illusion of as-if-serial semantics, which means that in a single-threaded program, the program should not be able to observe the effects of reorderings.However, reorderings can come into play in incorrectly synchronized multithreaded programs, where one thread is able to observe the effects of other threads, and may be able to detect that variable accesses become visible to other threads in a different order than executed or specified in the program.
編譯器、運行時和硬件應該合力創造as-if-serial語義的錯覺,這意味着在單線程程序中,程序不應該能夠觀察到重新排序的效果。然而,重新排序可以 在不正確同步的多線程程序中發揮作用,其中一個線程能夠觀察其他線程的影響,並且可能能夠檢測到變量訪問對其他線程以與程序中執行或指定的順序不同的順序變得可見。
as-if-serial語義允許重排序,CPU層面的指令優化依然存在。在單線程中,這些優化並不會影響整體的執行結果,在多線程中,重排序會帶來可見性問題。
另外,為了保證as-if-serial語義是正確的,編譯器和處理器不會對存在依賴關系的操作進行指令重排序,因為這樣會影響程序的執行結果。我們來看下面這段代碼:
public void execute(){
int x=10; //1
int y=5; //2
int c=x+y; //3
}
上述代碼按照正常的執行順序應該是1、2、3,在多線程環境下,可能會出現2、1、3這樣的執行順序,但是一定不會出現3、2、1這樣的順序,因為3與1和2存在數據依賴關系,一旦重排序,就無法保證as-if-serial語義是正確的。