前言
在很多情況下,訪問一個程序變量(對象實例字段,類靜態(tài)字段和數(shù)組元素)可能會使用不同的順序執(zhí)行,而不是程序語義所指定的順序執(zhí)行。具體幾種情況,如下:

例如,如果一個線程寫入值到字段a,然后寫入值到字段b,而且b的值不依賴于a的值,那么,處理器就能夠自由的調整它們的執(zhí)行順序,而且緩沖區(qū)能夠在a之前刷新b的值到主內存。有許多潛在的重排序的來源,例如編譯器,JIT以及緩沖區(qū)。
所以,從Java源碼變成可以被機器(或虛擬機)識別執(zhí)行的程序,至少要經過編譯期和運行期。在這兩個期間,重排序分為兩類:編譯器重排序、處理器重排序(亂序執(zhí)行),分別對應編譯時和運行時環(huán)境。由于重排序的存在,指令實際的執(zhí)行順序,并不是源碼中看到的順序。
1 編譯器重排序
編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序,在不改變程序語義的前提下,盡可能減少寄存器的讀取、存儲次數(shù),充分復用寄存器的存儲值。
假設第一條指令計算一個值賦給變量A并存放在寄存器中,第二條指令與A無關但需要占用寄存器(假設它將占用A所在的那個寄存器),第三條指令使用A的值且與第二條指令無關。那么如果按照順序一致性模型,A在第一條指令執(zhí)行過后被放入寄存器,在第二條指令執(zhí)行時A不再存在,第三條指令執(zhí)行時A重新被讀入寄存器,而這個過程中,A的值沒有發(fā)生變化。通常編譯器都會交換第二和第三條指令的位置,這樣第一條指令結束時A存在于寄存器中,接下來可以直接從寄存器中讀取A的值,降低了重復讀取的開銷。
另一種編譯器優(yōu)化:在循環(huán)中讀取變量的時候,為提高存取速度,編譯器會先把變量讀取到一個寄存器中;以后再取該變量值時,就直接從寄存器中取,不會再從內存中取值了。這樣能夠減少不必要的訪問內存。但是提高效率的同時,也引入了新問題。如果別的線程修改了內存中變量的值,那么由于寄存器中的變量值一直沒有發(fā)生改變,很有可能會導致循環(huán)不能結束。編譯器進行代碼優(yōu)化,會提高程序的運行效率,但是也可能導致錯誤的結果。所以程序員需要防止編譯器進行錯誤的優(yōu)化。
2 處理器重排序
2.1 指令并行重排序
編譯器和處理器可能會對操作做重排序,但是要遵守數(shù)據(jù)依賴關系,編譯器和處理器不會改變存在數(shù)據(jù)依賴關系的兩個操作的執(zhí)行順序。如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分下列三種類型:

上面三種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結果將會被改變。像這種有直接依賴關系的操作,是不會進行重排序的。特別注意:這里說的依賴關系僅僅是在單個線程內。
舉例:

由于操作 1 和 2 沒有數(shù)據(jù)依賴關系,編譯器和處理器可以對這兩個操作重排序;操作 3 和操作 4 沒有數(shù)據(jù)依賴關系,編譯器和處理器也可以對這兩個操作重排序。
當操作 1 和操作 2 重排序時,可能會產生什么效果?

如上圖所示,操作 1 和操作 2 做了重排序。程序執(zhí)行時,線程 A 首先寫標記變量 flag,隨后線程 B 讀這個變量。由于條件判斷為真,線程 B 將讀取變量 a。此時,變量 a 還根本沒有被線程 A 寫入,在這里多線程程序的語義被重排序破壞了!
當操作 3 和操作 4 重排序時,可能會產生什么效果?(借助這個重排序,可以順便說明控制依賴性)

在程序中,操作 3 和操作 4 存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關性對并行度的影響。以處理器的猜測執(zhí)行為例:

從圖中我們可以看出,猜測執(zhí)行?實質上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義!

2.2 指令亂序重排序
現(xiàn)在的CPU一般采用流水線來執(zhí)行指令。一個指令的執(zhí)行被分成:取指、譯碼、訪存、執(zhí)行、寫回、等若干個階段。然后,多條指令可以同時存在于流水線中,同時被執(zhí)行。指令流水線并不是串行的,并不會因為一個耗時很長的指令在“執(zhí)行”階段呆很長時間,而導致后續(xù)的指令都卡在“執(zhí)行”之前的階段上。相反,流水線是并行的,多個指令可以同時處于同一個階段,只要CPU內部相應的處理部件未被占滿即可。比如:CPU有一個加法器和一個除法器,那么一條加法指令和一條除法指令就可能同時處于“執(zhí)行”階段,而兩條加法指令在“執(zhí)行”階段就只能串行工作。
然而,這樣一來,亂序可能就產生了。比如:一條加法指令原本出現(xiàn)在一條除法指令的后面,但是由于除法的執(zhí)行時間很長,在它執(zhí)行完之前,加法可能先執(zhí)行完了。再比如兩條訪存指令,可能由于第二條指令命中了cache而導致它先于第一條指令完成。一般情況下,指令亂序并不是CPU在執(zhí)行指令之前刻意去調整順序。CPU總是順序的去內存里面取指令,然后將其順序的放入指令流水線。但是指令執(zhí)行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終亂序執(zhí)行完成。這就是所謂的“順序流入,亂序流出”。
指令流水線除了在資源不足的情況下會卡住之外(如前所述的一個加法器應付兩條加法指令的情況),指令之間的相關性也是導致流水線阻塞的重要原因。CPU的亂序執(zhí)行并不是任意的亂序,而是以保證程序上下文因果關系為前提的。有了這個前提,CPU執(zhí)行的正確性才有保證。
比如:

由于b=f(a)這條指令依賴于前一條指令a++的執(zhí)行結果,所以b=f(a)將在 “執(zhí)行” 階段之前被阻塞,直到a++的執(zhí)行結果被生成出來;而c--跟前面沒有依賴,它可能在b=f(a)之前就能執(zhí)行完。(注意,這里的f(a)并不代表一個以a為參數(shù)的函數(shù)調用,而是代表以a為操作數(shù)的指令。C語言的函數(shù)調用是需要若干條指令才能實現(xiàn)的,情況要更復雜些)。
像這樣有依賴關系的指令如果挨得很近,后一條指令必定會因為等待前一條執(zhí)行的結果,而在流水線中阻塞很久,占用流水線的資源。而編譯器的重排序,作為編譯優(yōu)化的一種手段,則試圖通過指令重排將這樣的兩條指令拉開距離,以至于后一條指令進入CPU的時候,前一條指令結果已經得到了,那么也就不再需要阻塞等待了。比如,將指令重排序為:

相比于CPU指令的亂序,編譯器的亂序才是真正對指令順序做了調整。但是編譯器的亂序也必須保證程序上下文的因果關系不發(fā)生改變。
由于重排序和亂序執(zhí)行的存在,如果在并發(fā)編程中,沒有做好共享數(shù)據(jù)的同步,很容易出現(xiàn)各種看似詭異的問題。