天天看點

Java的volatile到底該如何了解?(下)記憶體屏障

對于Long和double型變量的特殊規則

虛拟機規範中,寫64位的double和long分成了兩次32位值的操作

由于不是原子操作,可能導緻讀取到某次寫操作中64位的前32位,以及另外一次寫操作的後32位

讀寫volatile的long和double總是原子的。讀寫引用也總是原子的

商業JVM不會存在這個問題,雖然規範沒要求實作原子性,但是考慮到實際應用,大部分都實作了原子性。

對于32位平台,64位的操作需要分兩步來進行,與主存的同步。是以可能出現“半個變量”的狀态。

Java的volatile到底該如何了解?(下)記憶體屏障

在實際開發中,目前各種平台下的商用虛拟機幾乎都選擇把64位資料的讀寫操作作為原子操作來對待,是以我們在編碼時一般不需要把用到的long和double變量專門聲明為volatile。

Word Tearing位元組處理

一個字段或元素的更新不得與任何其他字段或元素的讀取或更新互動。

特别是,分别更新位元組數組的相鄰元素的兩個線程不得幹涉或互動,也不需要同步以確定順序一緻性。

有些處理器(尤其是早期的Alphas處理器)沒有提供寫單個位元組的功能。

在這樣的處理器_上更新byte數組,若隻是簡單地讀取整個内容,更新對應的位元組,然後将整個内容再寫回記憶體,将是不合法的。

這個問題有時候被稱為“字分裂(word tearing)”,在單獨更新單個位元組有難度的處理器上,就需要尋求其它方式了。

基本不需要考慮這個,了解就好。

JAVA代碼層級 - volatile
JVM層級 - JSR
os - 具體實作      

記憶體屏障

JSR的記憶體屏障(JVM 規範)

這隻是 JVM 層級的要求,非底層硬體的具體實作!

  • 在 volatile 讀寫前後都加上屏障
  • Java的volatile到底該如何了解?(下)記憶體屏障

LoadLoad屏障

對于這樣的語句Load1; Loadload; Load2,

在Load2及後續讀取操作要讀取的資料被通路前,保證Load1要讀取的資料被讀取完畢

StoreStore屏障

對于這樣的語句Store1; StoreStore; Store2,

在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

LoadStore屏障

對于這樣的語甸oad1; LoadStore; Store2,

在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。

Storeload屏障

對于這樣的語句Store1; StoreL oad; Ioad2,

在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。

JVM 層面 volatile的實作細節

Java的volatile到底該如何了解?(下)記憶體屏障

x86 CPU 記憶體屏障 - 原語級别實作

之是以JVM不直接使用這些指令,是因為并非所有 cpu 都支援,但是所有 cpu 都支援 lock 指令!

lock 指令直接鎖定總線,肯定直接禁止了重排序,是以 JVM是調用了該指令,簡單暴力!

Java的volatile到底該如何了解?(下)記憶體屏障
  • sfence

    在sfence指令前的寫操作當必須在sfence指令後的寫操作前完成

  • lfence

    在Ifence指令前的讀操作當必須在Ifence指令後的讀操作前完成

  • mfence

    在mfence指令前的讀寫操作當必須在mfence指令後的讀寫操作前完成。

  • 有序性保障:intel lock 彙編指令

原子指令,如x86上的lock指令是一個Full Barrier,執行時會鎖住記憶體子系統來確定執行順序,甚至跨多個CPU。Software Locks通常使用了記憶體屏障或原子指令來實作變量可見性和保持程式順序

處理器提供了兩個記憶體屏障指令(Memory Barrier)用于解決上述的兩個問題:

7.1 指令分類

  • 寫記憶體屏障(Store Memory Barrier)

    在指令後插入Store Barrier,能讓寫入緩存中的最新資料更新寫入主記憶體,讓其他線程可見

強制寫入主記憶體,這種顯示調用,CPU就不會因為性能考慮而去對指令重排

讀記憶體屏障(Load Memory Barrier)

在指令前插入Load Barrier,可以讓高速緩存中的數

據失效,強制從新從主記憶體加載資料。

強制讀取主記憶體内容,讓CPU緩存與主記憶體保持一緻,避免了緩存導緻的一緻性問題

7.2 有序性(Ordering)

JMM中程式的天然有序性可以總結為一句話:

如果在本線程内觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。

前半句是指“線程内表現為串行語義”

後半句是指“指令重排序”現象和“工作記憶體主主記憶體同步延遲”現象

Java提供了volatile和synchronized保證線程之間操作的有序性

volatile本身就包含了禁止指令重排序的語義

synchronized則是由“一個變量在同一時刻隻允許一條線程對其進行lock操作”這條規則來獲得的,這個規則決定了持有同一個鎖的兩個同步塊隻能串行地進入。

7.3 Happens-beofre 先行發生原則(JVM 規範)

這八種不能指令重排序

如果JMM中所有的有序性都隻靠volatile和synchronized,那麼有一些操作将會變得很繁瑣,但我們在編寫Java并發代碼時并沒有感到這一點,這是因為Java語言中有一個先行發生(Happen-Before)原則。它是判斷資料是否存在競争,線程是否安全的主要依賴。

先行發生原則

JMM中定義的兩項操作之間的依序關系。

happens- before關系 主要是強調兩個有沖突的動作之間的順序,以及定義資料争用的發生時機。

如果操作A先行發生于操作B,就是在說發生B前,A産生的影響能被B觀察到,“影響”包含了修改記憶體中共享變量的值、發送了消息、調用了方法等。案例如下:

// 線程A中執行  
i = 1;  

// 線程B中執行  
j = i;  

// 線程C中執行  
i = 2;      

下面是JMM下一些”天然的“先行發生關系,無須任何同步器協助就已經存在,可以在編碼中直接使用。

如果兩個操作之間的關系不在此列,并且無法從下列規則推導出來的話,它們就沒有順序性保障,虛拟機可以對它們進行随意重排序。

具體的虛拟機實作,有必要確定以下原則的成立:

程式次序規則(Pragram Order Rule)

在一個線程内,按照代碼順序,書寫在前面的操作先行發生于書寫在後面的操作。準确地說應該是控制流順序而不是程式代碼順序,因為要考慮分支、循環結構。

對象鎖(螢幕鎖)法則(Monitor Lock Rule )

某個管程(也叫做對象鎖,螢幕鎖) 上的unlock動作happens-before同一個管程上後續的lock動作 。這裡必須強調的是同一個鎖,而”後面“是指時間上的先後。

volatile變量規則(Volatile Variable Rule)

對某個volatile字段的寫操作happens- before每個後續對該volatile字段的讀操作,這裡的”後面“同樣指時間上的先後順序。

線程啟動規則(Thread Start Rule)

在某個線程對象 上調用start()方法happens- before該啟動了的線程中的任意動作

線程終止規則(Thread Termination Rule)

某線程中的所有操作都先行發生于對此線程的終止檢測,我們可以通過Thread.join()方法結束(任意其它線程成功從該線程對象上的join()中傳回),Thread.isAlive()的傳回值等作段檢測到線程已經終止執行。

線程中斷規則(Thread Interruption Rule)

對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測是否有中斷發生

對象終結規則(Finalizer Rule)

一個對象初始化完成(構造方法執行完成)先行發生于它的finalize()方法的開始

傳遞性(Transitivity)

如果操作A先行發生于操作B,操作B先行發生于操作C,那就可以得出操作A先行發生于操作C的結論

一個操作”時間上的先發生“不代表這個操作會是”先行發生“,那如果一個操作”先行發生“是否就能推導出這個操作必定是”時間上的先發生“呢?也是不成立的,一個典型的例子就是指令重排序。

是以時間上的先後順序與先行發生原則之間基本沒有什麼關系,是以衡量并發安全問題一切必須以先行發生原則為準。

7.4 作用

1.阻止屏障兩側的指令重排序

2.強制把寫緩沖區/高速緩存中的髒資料等寫回主記憶體,讓緩存中相應的資料失效

對于Load Barrier來說,在指令前插入Load Barrier,可以讓高速緩存中的資料失效,強制從新從主記憶體加載資料

對于Store Barrier來說,在指令後插入Store Barrier,能讓寫入緩存中的最新資料更新寫入主記憶體,讓其他線程可見

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的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實作中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能

volatile的記憶體屏障政策非常嚴格保守

在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障

在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障

由于記憶體屏障的作用,避免了volatile變量和其它指令重排序、線程之間實作了通信,使得volatile表現出了鎖的特性。

總結

看到了現代CPU不斷演進,在程式運作優化中做出的努力。不同CPU廠商所付出的人力物力成本,最終展現在不同CPU性能差距上。而Java就随即推出了大量保證線程安全的機