我是否是學了一門假的java。。。。。。html
引言:在Java中看似順序的代碼在JVM中,可能會出現編譯器或者CPU對這些操做指令進行了從新排序;在特定狀況下,指令重排將會給咱們的程序帶來不肯定的結果.....java
(帶來個毛的不肯定,他奶奶的多線程只存在於學習Java基礎,實際工做中用的不多,除非是本身造輪子;因此我寫這個算不算鹹吃蘿蔔淡操心捏?)程序員
本文大部分來自於:Java內存訪問重排序的研究,想看原做請移步。編程
以下代碼可能的結果有哪些?緩存
public class PossibleReordering { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); one.join();other.join(); System.out.println("(" + x + "," + y + ")"); }
}
看完本文你就明白了。(明白了也沒啥卵用,別看了)性能優化
(看這個是否是感受很牛逼的詞,感受是否是要學習一下編譯原理,反正看了以後發現發現學習編譯原理不只僅在於去開發一門編譯器,還在於對語言的深度學習啊)多線程
在計算機執行指令的順序在通過程序編譯器編譯以後造成的指令序列,通常而言,這個指令序列是會輸出肯定的結果;以確保每一次的執行都有肯定的結果。可是,通常狀況下,CPU和編譯器爲了提高程序執行的效率,會按照必定的規則容許進行指令優化。架構
爲雞毛重排能夠提升代碼的執行效率?併發
大多數現代微處理器都會採用將指令亂序執行(out-of-order execution,簡稱OoOE或OOE)的方法,在條件容許的狀況下,直接運行當前有能力當即執行的後續指令,避開獲取下一條指令所需數據時形成的等待。經過亂序執行的技術,處理器能夠大大提升執行效率。
除了處理器,常見的Java運行時環境的JIT編譯器也會作指令重排序操做,即生成的機器指令與字節碼指令順序不一致。oracle
(CPU要的指令重排是否是從另外一方面告誡咱們寫代碼的方向呢,不只處理器欺負咱們,連javac都欺負咱們,555,不能好好開發了)
在某些狀況下,這種優化會帶來一些執行的邏輯問題,主要的緣由是代碼邏輯之間是存在必定的前後順序,在併發執行狀況下,會發生二義性,即按照不一樣的執行邏輯,會獲得不一樣的結果信息。
主要指不一樣的程序指令之間的順序是不容許進行交換的,便可稱這些程序指令之間存在數據依賴性。
哪些指令不容許重排?
主要的例子以下:
名稱 代碼示例 說明
寫後讀 a = 1;b = a; 寫一個變量以後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變量以後,再寫這個變量。
讀後寫 a = b;b = 1; 讀一個變量以後,再寫這個變量。
進過度析,發現這裏每組指令中都有寫操做,這個寫操做的位置是不容許變化的,不然將帶來不同的執行結果。
編譯器將不會對存在數據依賴性的程序指令進行重排,這裏的依賴性僅僅指單線程狀況下的數據依賴性;多線程併發狀況下,此規則將失效。
無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。
分析: 關鍵詞是單線程狀況下,必須遵照;其他的不遵照。
as-if-serial語義是啥?
as-if-serial語義的意思是,全部的動做(Action)均可覺得了優化而被重排序,可是必須保證它們重排序後的結果和程序代碼自己的應有結果是一致的。Java編譯器、運行時和處理器都會保證單線程下的as-if-serial語義。
好比,爲了保證這一語義,重排序不會發生在有數據依賴的操做之中。
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
分析代碼:
A->C B->C; A,B之間不存在依賴關係; 故在單線程狀況下, A與B的指令順序是能夠重排的,C不容許重排,必須在A和B以後。
as-if-serial語義把單線程程序保護了起來,遵照as-if-serial語義的編譯器,runtime 和處理器共同爲編寫單線程程序的程序員建立了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題。核心點仍是單線程,多線程狀況下不遵照此原則。
從這裏開始,不少都是拷貝的,水平有限,拷貝來補,逃~~~
計算機系統中,爲了儘量地避免處理器訪問主內存的時間開銷,處理器大多會利用緩存(cache)以提升性能。其模型以下圖所示。
在這種模型下會存在一個現象,即緩存中的數據與主內存的數據並非實時同步的,各CPU(或CPU核心)間緩存的數據也不是實時同步的。這致使在同一個時間點,各CPU所看到同一內存地址的數據的值多是不一致的。從程序的視角來看,就是在同一個時間點,各個線程所看到的共享變量的值多是不一致的。
有的觀點會將這種現象也視爲重排序的一種,命名爲"內存系統重排序"。由於這種內存可見性問題形成的結果就好像是內存訪問指令發生了重排序同樣。
這種內存可見性問題也會致使文章最開頭示例代碼即使在沒有發生指令重排序的狀況下的執行結果也仍是(0, 0)。
Java的目標是成爲一門平臺無關性的語言,即Write once, run anywhere。可是不一樣硬件環境下指令重排序的規則不盡相同。例如,x86下運行正常的Java程序在IA64下就可能獲得非預期的運行結果。爲此,JSR-1337制定了Java內存模型(Java Memory Model, JMM),旨在提供一個統一的可參考的規範,屏蔽平臺差別性。從Java 5開始,Java內存模型成爲Java語言規範的一部分。
根據Java內存模型中的規定,能夠總結出如下幾條happens-before規則。Happens-before的先後兩個操做不會被重排序且後者對前者的內存可見。
Happens-before關係只是對Java內存模型的一種近似性的描述,它並不夠嚴謹,但便於平常程序開發參考使用,關於更嚴謹的Java內存模型的定義和描述,請閱讀JSR-133原文或Java語言規範章節17.4。
除此以外,Java內存模型對volatile和final的語義作了擴展。對volatile語義的擴展保證了volatile變量在一些狀況下不會重排序,volatile的64位變量double和long的讀取和賦值操做都是原子的。對final語義的擴展保證一個對象的構建方法結束前,全部final成員變量都必須完成初始化(前提是沒有this引用溢出,這裏不該該用溢出,而是逸出,呵呵)。
Java內存模型關於重排序的規定,總結後以下表所示。
表中"第二項操做"的含義是指,第一項操做以後的全部指定操做。如,普通讀不能與其以後的全部volatile寫重排序。另外,JMM也規定了上述volatile和同步塊的規則盡適用於存在多線程訪問的情景。例如,若編譯器(這裏的編譯器也包括JIT,下同)證實了一個volatile變量只能被單線程訪問,那麼就可能會把它作爲普通變量來處理。
留白的單元格表明容許在不違反Java基本語義的狀況下重排序。例如,編譯器不會對對同一內存地址的讀和寫操做重排序,可是容許對不一樣地址的讀和寫操做重排序。
除此以外,爲了保證final的新增語義。JSR-133對於final變量的重排序也作了限制。
首先咱們基於一段代碼的示例來分析,在多線程狀況下,重排是否有不一樣結果信息:
class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a * a; // 4 } } }
上述的代碼,在單線程狀況下,執行結果是肯定的, flag=true將被reader的方法體中看到,並正確的設置結果。 可是在多線程狀況下,是否仍是隻有一個肯定的結果呢?
假設有A和B兩個線程同時來執行這個代碼片斷, 兩個可能的執行流程以下:
可能的流程1, 因爲1和2語句之間沒有數據依賴關係,故二者能夠重排,在兩個線程之間的可能順序以下:
可能的流程2:, 在兩個線程之間的語句執行順序以下:
根據happens-before的程序順序規則,上面計算圓的面積的示例代碼存在三個happens-before關係:
啥是happens-before關係?
A happens-before B;
B happens-before C;
A happens-before C;
這裏的第3個happens- before關係,是根據happens-before的傳遞性推導出來的
啥是控制依賴關係?
啥是猜想(Speculation)?
在程序中,操做3和操做4存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。爲此,編譯器和處理器會採用猜想(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜想執行爲例,執行線程B的處理器能夠提早讀取並計算a*a,而後把計算結果臨時保存到一個名爲重排序緩衝(reorder buffer ROB)的硬件緩存中。當接下來操做3的條件判斷爲真時,就把該計算結果寫入變量i中。從圖中咱們能夠看出,猜想執行實質上對操做3和4作了重排序。重排序在這裏破壞了多線程程序的語義。
與上面的例子相似的有:
在線程A中:
context = loadContext(); inited = true;
while(!inited ){ //根據線程A中對inited變量的修改決定是否使用context變量 sleep(100); } doSomethingwithconfig(context);
inited = true; context = loadContext();
重排致使雙重鎖定的單例模式失效的例子
例子2:指令重排致使單例模式失效
咱們都知道一個經典的懶加載方式的雙重判斷單例模式:
public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance == null) { synchronzied(Singleton.class) { if(instance == null) { instance = new Singleton(); //非原子操做 } } } return instance; } }
memory =allocate(); //1:分配對象的內存空間 ctorInstance(memory); //2:初始化對象 instance =memory; //3:設置instance指向剛分配的內存地址
memory =allocate(); //1:分配對象的內存空間 instance =memory; //3:instance指向剛分配的內存地址,此時對象還未初始化 ctorInstance(memory); //2:初始化對象
解決方案:例子1中的inited和例子2中的instance以關鍵字volatile修飾以後,就會阻止JVM對其相關代碼進行指令重排,這樣就可以按照既定的順序指執行。
核心點是:兩個線程之間在執行同一段代碼之間的critical area,在不一樣的線程之間共享變量;因爲執行順序、CPU編譯器對於程序指令的優化等形成了不肯定的執行結果。
我感受這種狀況大多發生多線程環境下:在你先去判斷,而後決定作出操做時容易出錯,對你要判斷的對象加個volatile是個不錯的選擇。
volatile關鍵字能夠保證變量的可見性,由於對volatile的操做都在Main Memory中,而Main Memory是被全部線程所共享的,這裏的代價就是犧牲了性能,沒法利用寄存器或Cache,由於它們都不是全局的,沒法保證可見性,可能產生髒讀。
volatile還有一個做用就是局部阻止重排序的發生(在JDK1.5以後,可使用volatile變量禁止指令重排序),對volatile變量的操做指令都不會被重排序,由於若是重排序,又可能產生可見性問題。
在保證可見性方面,鎖(包括顯式鎖、對象鎖)以及對原子變量的讀寫均可以確保變量的可見性。
可是實現方式略有不一樣,例如同步鎖保證獲得鎖時從內存裏從新讀入數據刷新緩存,釋放鎖時將數據寫回內存以保數據可見,而volatile變量乾脆都是讀寫內存。
volatile關鍵字經過提供"內存屏障"的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
大多數的處理器都支持內存屏障的指令。
對於編譯器來講,發現一個最優佈置來最小化插入屏障的總數幾乎不可能,爲此,Java內存模型採起保守策略。下面是基於保守策略的JMM內存屏障插入策略:
在每一個volatile寫操做的前面插入一個StoreStore屏障。
在每一個volatile寫操做的後面插入一個StoreLoad屏障。
在每一個volatile讀操做的後面插入一個LoadLoad屏障。
在每一個volatile讀操做的後面插入一個LoadStore屏障。
內存屏障(Memory Barrier,或有時叫作內存柵欄,Memory Fence)是一種CPU指令,用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。
內存屏障能夠被分爲如下幾種類型
LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操做要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操做執行前,保證Store1的寫入操做對其它處理器可見。
LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操做被刷出前,保證Load1要讀取的數據被讀取完畢。
StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續全部讀取操做執行前,保證Store1的寫入對全部處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。
有的處理器的重排序規則較嚴,無需內存屏障也能很好的工做,Java編譯器會在這種狀況下不放置內存屏障。
爲了實現前面討論的JSR-133的規定,Java編譯器會這樣使用內存屏障。
爲了保證final字段的特殊語義,也會在下面的語句加入內存屏障。
x.finalField = v; StoreStore; sharedRef = x;
Intel 64和IA-32是咱們較經常使用的硬件環境,相對於其它處理器而言,它們擁有一種較嚴格的重排序規則。Pentium 4之後的Intel 64或IA-32處理的重排序規則以下。
在單CPU系統中
在多處理器系統中
值得注意的是,對於Java編譯器而言,Intel 64/IA-32架構下處理器不須要LoadLoad、LoadStore、StoreStore屏障,由於不會發生須要這三種屏障的重排序。
如今有這樣一個場景,一個容器能夠放一個東西,容器支持create方法來建立一個新的東西並放到容器裏,支持get方法取到這個容器裏的東西。咱們能夠較容易地寫出下面的代碼。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private SomeThing object; public void create() { object = new SomeThing(); } public SomeThing get() { while (object == null) { Thread.yield(); //不加這句話可能會在此出現無限循環 } return object; } }
在單線程場景下,這段代碼執行起來是沒有問題的。可是在多線程併發場景下,由不一樣的線程create和get東西,這段代碼是有問題的。問題的緣由與普通的雙重檢查鎖定單例模式(Double Checked Locking, DCL)10相似,即SomeThing的構建與將指向構建中的SomeThing引用賦值到object變量這二者可能會發生重排序。致使get中返回一個正被構建中的不完整的SomeThing對象實例。爲了解決這一問題,一般的辦法是使用volatile修飾object字段。這種方法避免了重排序,保證了內存可見性,摒棄比使用同步塊致使的性能損失更小。可是,假如使用場景對object的內存可見性並不敏感的話(不要求一個線程寫入了object,object的新值當即對下一個讀取的線程可見),在Intel 64/IA-32環境下,有更好的解決方案。
根據上一章的內容,咱們知道Intel 64/IA-32下寫操做之間不會發生重排序,即在處理器中,構建SomeThing對象與賦值到object這兩個操做之間的順序性是能夠保證的。這樣看起來,僅僅使用volatile來避免重排序是畫蛇添足的。可是,Java編譯器卻可能生成重排序後的指令。但使人高興的是,Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong這三個方法,JDK會在執行這三個方法時插入StoreStore內存屏障,避免發生寫操做重排序。而在Intel 64/IA-32架構下,StoreStore屏障並不須要,Java編譯器會將StoreStore屏障去除。比起寫入volatile變量以後執行StoreLoad屏障的巨大開銷,採用這種方法除了避免重排序而帶來的性能損失之外,不會帶來其它的性能開銷。
咱們將作一個小實驗來比較兩者的性能差別。一種是使用volatile修飾object成員變量。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private volatile SomeThing object; public void create() { object = new SomeThing(); } public SomeThing get() { while (object == null) { Thread.yield(); //不加這句話可能會在此出現無限循環 } return object; } }
一種是利用Unsafe. putOrderedObject在避免在適當的位置發生重排序。
public class Container { public static class SomeThing { private int status; public SomeThing() { status = 1; } public int getStatus() { return status; } } private SomeThing object; private Object value; private static final Unsafe unsafe = getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset(Container.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } public void create() { SomeThing temp = new SomeThing(); unsafe.putOrderedObject(this, valueOffset, null); //將value賦null值只是一項無用操做,實際利用的是這條語句的內存屏障 object = temp; } public SomeThing get() { while (object == null) { Thread.yield(); } return object; } public static Unsafe getUnsafe() { try { Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); return (Unsafe)f.get(null); } catch (Exception e) { } return null; } }
因爲直接調用Unsafe.getUnsafe()須要配置JRE獲取較高權限,咱們利用反射獲取Unsafe中的theUnsafe來取得Unsafe的可用實例。
unsafe.putOrderedObject(this, valueOffset, null)
這句僅僅是爲了借用這句話功能的防止寫重排序,除此以外無其它做用。
利用下面的代碼分別測試兩種方案的實際運行時間。在運行時開啓-server和 -XX:CompileThreshold=1以模擬生產環境下長時間運行後的JIT優化效果。
public static void main(String[] args) throws InterruptedException { final int THREADS_COUNT = 20; final int LOOP_COUNT = 100000; long sum = 0; long min = Integer.MAX_VALUE; long max = 0; for(int n = 0;n <= 100;n++) { final Container basket = new Container(); List<Thread> putThreads = new ArrayList<Thread>(); List<Thread> takeThreads = new ArrayList<Thread>(); for (int i = 0; i < THREADS_COUNT; i++) { putThreads.add(new Thread() { @Override public void run() { for (int j = 0; j < LOOP_COUNT; j++) { basket.create(); } } }); takeThreads.add(new Thread() { @Override public void run() { for (int j = 0; j < LOOP_COUNT; j++) { basket.get().getStatus(); } } }); } long start = System.nanoTime(); for (int i = 0; i < THREADS_COUNT; i++) { takeThreads.get(i).start(); putThreads.get(i).start(); } for (int i = 0; i < THREADS_COUNT; i++) { takeThreads.get(i).join(); putThreads.get(i).join(); } long end = System.nanoTime(); long period = end - start; if(n == 0) { continue; //因爲JIT的編譯,第一次執行須要更多時間,將此時間不計入統計 } sum += (period); System.out.println(period); if(period < min) { min = period; } if(period > max) { max = period; } } System.out.println("Average : " + sum / 100); System.out.println("Max : " + max); System.out.println("Min : " + min); }
在筆者的計算機上運行測試,採用volatile方案的運行結果以下
Average : 62535770
Max : 82515000
Min : 45161000
採用unsafe.putOrderedObject方案的運行結果以下
Average : 50746230
Max : 68999000
Min : 38038000
從結果看出,unsafe.putOrderedObject方案比volatile方案平均耗時減小18.9%,最大耗時減小16.4%,最小耗時減小15.8%.另外,即便在其它會發生寫寫重排序的處理器中,因爲StoreStore屏障的性能損耗小於StoreLoad屏障,採用這一方法也是一種可行的方案。但值得再次注意的是,這一方案不是對volatile語義的等價替換,而是在特定場景下作的特殊優化,它僅避免了寫寫重排序,但不保證內存可見性。
參考文獻
https://tech.meituan.com/java-memory-reordering.html
http://en.wikipedia.org/wiki/Out-of-order_execution
Oracle Java Hotspot https://wikis.oracle.com/display/HotSpotInternals/PerformanceTacticIndex IBM JVM http://publib.boulder.ibm.com/infocenter/javasdk/v1r4m2/index.jsp?topic=%2Fcom.ibm.java.doc.diagnostics.142j9%2Fhtml%2Fhowjitopt.html
Java語言規範中對「動做」這個詞有一個明確而具體的定義,詳見http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.2。
https://community.oracle.com/thread/1544959
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
參見《Java併發編程實踐》章節16.1
Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide章節8.2
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
抄得太多,消化不良