1、記憶體可見性
首先,要明確一下這個記憶體的含義,記憶體包括共享主存和快取記憶體,Volatile關鍵字標識的變數,是指CPU從快取讀取資料時,要判斷資料是否有效,如果快取沒有資料,則再從主存讀取,主存就不存在是否有效的說法了。而記憶體一致性協議也是針對快取的協議。
記憶體可見性意思是:一個CPU核心對資料的修改,對其他CPU核心立即可見。
這句話拆開了理解,CPU修改資料,首先是對快取的修改,然後再同步回主存,在同步回主存的時候,如果其他CPU也快取了這個資料,就會導致其他CPU快取上的資料失效,這樣,當其他CPU再去它的快取讀取這個資料的時候,就必須從主存重新獲取。
實現的原理一般都是基於CPU的MESI協議,其中E表示獨佔Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一個核心修改了資料,那麼這個核心的資料狀態就會更新成M,同時其他核心上的資料狀態更新成I,這個是透過CPU多核之間的嗅探機制實現的。
但是,這樣是否就能保證多執行緒操作一個變數的時候,結果如我們預期呢?其實不然。
因為,Volatile限定的是從快取讀取時刻的校驗,如果兩個CPU同時從各自快取讀取一個變數n=1(此時,變數n在各個CPU快取上都是有效的),並且同時修改了變數n=n+1,再寫回快取,這個時候n的值等於2,而不是等於3。因此,在多執行緒操作共享變數(例如:計數器)的時候,正確的方式是使用同步或者Atomic工具類。
2、指令有序性
這個涉及到記憶體屏障(Memory Barrier),記憶體屏障有兩個能力:
a、就像一套柵欄分割前後的程式碼,阻止柵欄前後的沒有資料依賴性的程式碼進行指令重排序,保證程式在一定程度上的有序性。
b、強制把寫緩衝區/快取記憶體中的髒資料等寫回主記憶體,讓快取中相應的資料失效,保證資料的可見性。
首先,指令並不是程式碼行,指令是原子的,透過javap命令可以看到一行程式碼編譯出來的指令,當然,像int i=1;這樣的程式碼行也是原子操作。
在單例模式中,Instance inst = new Instance();
這一句,就不是原子操作,它可以分成三步原子指令:
1,分配記憶體地址;
2,new一個Instance物件;
3,將記憶體地址賦值給inst;
CPU為了提高執行效率,這三步操作的順序可以是123,也可以是132,如果是132順序的話,當把記憶體地址賦給inst後,inst指向的記憶體地址上面還沒有new出來單例物件,這時候,如果就拿到inst的話,它其實就是空的,會報空指標異常。這就是為什麼單例模式中,單例物件要加上volatile關鍵字。
記憶體屏障有三種類型和一種偽型別:
a、lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓快取記憶體中的資料失效,重新從主記憶體載入資料,以保證讀取的是最新的資料。
b、sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入快取的最新資料寫回到主記憶體,以保證寫入的資料立刻對其他執行緒可見。
c、mfence,即全能屏障,具備ifence和sfence的能力。
d、Lock字首:Lock不是一種記憶體屏障,但是它能完成類似全能型記憶體屏障的功能。
---------------------
1、記憶體可見性
首先,要明確一下這個記憶體的含義,記憶體包括共享主存和快取記憶體,Volatile關鍵字標識的變數,是指CPU從快取讀取資料時,要判斷資料是否有效,如果快取沒有資料,則再從主存讀取,主存就不存在是否有效的說法了。而記憶體一致性協議也是針對快取的協議。
記憶體可見性意思是:一個CPU核心對資料的修改,對其他CPU核心立即可見。
這句話拆開了理解,CPU修改資料,首先是對快取的修改,然後再同步回主存,在同步回主存的時候,如果其他CPU也快取了這個資料,就會導致其他CPU快取上的資料失效,這樣,當其他CPU再去它的快取讀取這個資料的時候,就必須從主存重新獲取。
實現的原理一般都是基於CPU的MESI協議,其中E表示獨佔Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一個核心修改了資料,那麼這個核心的資料狀態就會更新成M,同時其他核心上的資料狀態更新成I,這個是透過CPU多核之間的嗅探機制實現的。
但是,這樣是否就能保證多執行緒操作一個變數的時候,結果如我們預期呢?其實不然。
因為,Volatile限定的是從快取讀取時刻的校驗,如果兩個CPU同時從各自快取讀取一個變數n=1(此時,變數n在各個CPU快取上都是有效的),並且同時修改了變數n=n+1,再寫回快取,這個時候n的值等於2,而不是等於3。因此,在多執行緒操作共享變數(例如:計數器)的時候,正確的方式是使用同步或者Atomic工具類。
2、指令有序性
這個涉及到記憶體屏障(Memory Barrier),記憶體屏障有兩個能力:
a、就像一套柵欄分割前後的程式碼,阻止柵欄前後的沒有資料依賴性的程式碼進行指令重排序,保證程式在一定程度上的有序性。
b、強制把寫緩衝區/快取記憶體中的髒資料等寫回主記憶體,讓快取中相應的資料失效,保證資料的可見性。
首先,指令並不是程式碼行,指令是原子的,透過javap命令可以看到一行程式碼編譯出來的指令,當然,像int i=1;這樣的程式碼行也是原子操作。
在單例模式中,Instance inst = new Instance();
這一句,就不是原子操作,它可以分成三步原子指令:
1,分配記憶體地址;
2,new一個Instance物件;
3,將記憶體地址賦值給inst;
CPU為了提高執行效率,這三步操作的順序可以是123,也可以是132,如果是132順序的話,當把記憶體地址賦給inst後,inst指向的記憶體地址上面還沒有new出來單例物件,這時候,如果就拿到inst的話,它其實就是空的,會報空指標異常。這就是為什麼單例模式中,單例物件要加上volatile關鍵字。
記憶體屏障有三種類型和一種偽型別:
a、lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓快取記憶體中的資料失效,重新從主記憶體載入資料,以保證讀取的是最新的資料。
b、sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入快取的最新資料寫回到主記憶體,以保證寫入的資料立刻對其他執行緒可見。
c、mfence,即全能屏障,具備ifence和sfence的能力。
d、Lock字首:Lock不是一種記憶體屏障,但是它能完成類似全能型記憶體屏障的功能。
---------------------