1. 程式人生 > 其它 >深究可見性,原子性,有序性的解決方案之記憶體屏障

深究可見性,原子性,有序性的解決方案之記憶體屏障

   在瞭解記憶體屏障之前,我們先了解一下JMM模型的8種原子操作:

1.lock 鎖定 : 把主記憶體中的一個變數標誌為一個執行緒獨享的狀態

2.unlock 解鎖 : 把主記憶體中的一個變數釋放出來

3.read 讀:將主記憶體中的變數讀到工作記憶體中

4.load 載入:將工作記憶體中的變數載入到副本中

5.use 使用:當執行引擎需要使用到一個變數時,將工作記憶體中的變數的值傳遞給執行引擎

6.assign 賦值:將執行引擎收的的值賦值給工作記憶體中的變數

7.store 儲存:將工作記憶體中的變數的值傳到主記憶體中

8.write 寫入:將store得到值放到主記憶體的變數中

並且JMM記憶體模型還規定了以上操作必須遵守的規則:

1.如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順尋地執行read和load操作, 如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

2.不允許read和load、store和write操作之一單獨出現

3.不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。

4.不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。

5.一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。

6.一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。lock和unlock必須成對出現

7.如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值

8.如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。

9.對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)

 

對於記憶體屏障而言,關注的就是store和load操作。

根據JMM模型規定,read和load,store和write必須同時出現,並且按照順序執行,所以執行完load操作,必然是載入了主記憶體的值的,但不能保證這兩操作時是原子性的,同樣的道理,執行store也是一樣的,都無法保證操作的原子性,那麼記憶體屏障又是如何解決這個問題的呢?

首先看硬體層面的記憶體屏障:

1. lfence,是一種Load Barrier 讀屏障。在讀指令前插入讀屏障,可以讓快取記憶體中的資料失效,重新從主記憶體載入資料,其實就是告訴作業系統,後面的值給我去主存中取。

2. sfence, 是一種Store Barrier 寫屏障。在寫指令之後插入寫屏障,能讓寫入快取的最新資料寫回到主記憶體,其實就是把我寫的這條資料直接刷到主存,看著和上面那條差不多都是刷最新的資料,但區別在於其他核心不一定會來取

3. mfence, 是一種全能型的屏障,具備ifence和sfence的能力,就是把當前快取行的資料修改過的最新值刷入主存,其他的失效,重新從主存獲取。
4. lock字首也能實現類似的效果,它通過對匯流排/快取行加鎖,執行後面的指令,這個時候所有訪問這條匯流排/快取行的請求都會被阻塞,直到鎖釋放。lock指令可以保證前面的修改都會重新整理到主存,並且釋放鎖後會使所有所有對應快取行失效,這樣就可以達到和記憶體屏障一樣的效果

java中unsafe包也提供了類似的屏障:

public native void loadFence(); // 讀屏障

public native void storeFence(); // 寫屏障

public native void fullFence(); //兩者都有

底層實現也都是差不多的,其實記憶體屏障的另外一個含義就是可以禁止重排序,屏障就是一道柵欄,前面的指令必須在前面執行完,後面的不能跳到前面執行,禁止程式碼過度優化。   最後我們再看下上面的問題,如何保證store load 和read write之間的原子性,最簡單的就是加鎖,但是這樣的消耗太大,而記憶體屏障通過一道柵欄的形式,並保證快取一致性,同樣也實現了寫與讀之間的原子性。