聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型
可以說并發系統要解決的最核心問題之一就是一致性的問題,關于一致性的研究已經有幾十年了,有大量的理論,算法支持。這篇說說一致性這個主題一些經常提到的概念,理清Java內存模型在其中的位置。
?
一致性問題更準確的說是一致性需求,看系統需要什么樣的一致性保證。比如分布式領域的CAP理論說Consistency, Availability, Partition tolerance這三個要求同時只能滿足兩個,另外一個就要有所取舍。所以很多種場景下,Consistency可能只需要滿足最終一致性,不用滿足強一致性。
?
一致性問題在單機器單CPU的情況下是最簡單的,由于只有一個CPU,所有的讀寫操作都可以按照全局的時間順序執行
在單機器多CPU的情況下,多CPU并發執行,共用一個內存,一般通過共享內存的方式來處理一致性問題,通過定義滿足不同一致性需求的內存模型來解決內存一致性問題(Memory Consistency)
在分布式環境中,多臺機器多CPU通過網絡來并發執行,一般通過消息通信的方式來處理一致性問題,比如分布式事務的多階段提交,處理分布式存儲的Paxos協議,ZooKeeper的Zab協議,處理的都是分布式存儲場景下的數據一致性的問題。分布式環境中也有使用分布式共享內存的方式。
?
所以目前處理一致性問題主要有共享內存和消息通信這兩個大的方式,每種方式里面又根據不同的需求有不同的實現方式。Java內存模型處理的就是單機器多CPU場景下的內存一致性問題
?
先來看看一致性的定義,這是馮諾依曼體系結構中對一致性的定義
?Consistency: a read returns the most recently written value
一個讀操作應該返回"最近"的一個寫操作寫入的值
?
但是"最近"(most recently)這個概念比較模糊,需要對其概念嚴格化,根據不同的嚴格化定義,這幾十年來產生了多種不同的一致性定義,每種一致性定義要解決的場景也都有區別。
1. 嚴格一致性 Strict Consistency 線性一致性?Linearizability?
這是最嚴格的概念模型,定義了在應用場景中,所有的讀寫操作都按照全局的時序來排列執行,比如在單機器多核CPU的場景下,所有的CPU需要共享一個全局的時鐘順序,并且所有CPU的任意讀寫操作都要按照這個全局的時鐘順序執行,一旦新寫入了一個值,那么這個值必須馬上被其他所有的CPU都能看到。在分布式場景下,所有的分布式節點都要共享一個全局的時鐘順序來執行。
嚴格一致性要求寫操作能夠馬上(instantaneously)被傳播出去,任意執行的節點要馬上可以看到這個新寫入的值。這個模型在數學上是可行的,但是在物理上是難以實現的,而且即使實現也是最低效率的,所以大家看看就好
?
2. 順序一致性 Sequential Consistency
順序一致性不要求全局的時鐘順序,它只需要各個CPU局部的時鐘順序,它由三個要點
- 對每個單個CPU來說,它看到自己程序的執行順序始終是和程序定義是一致的(單個CPU角度)
- 每個CPU看到的其他CPU的寫操作都是按照相同的順序執行的,大家看到的最終執行的視圖是一致的(從全局的角度)
- 單個CPU對共享變量的寫操作馬上對其他CPU可見
這篇為什么程序員需要關心順序一致性(Sequential Consistency)而不是Cache一致性(Cache Coherence)的例子很好,很能說明順序一致性的特點,下面的例子來自這篇文章。
根據順序一致性的特點,我們知道r1和r2的只能有這3種結果,因為順序一致性允許不同的CPU并發執行,但是對單個CPU的指令來說是按照執行的程序順序執行的,所以不會出現r1 = y先于x=1執行的情況。并且所有的處理器都只會看到同一個全局的執行順序,要么Execution1,或者2,或者3,不會出現兩個處理器看到不同的全局執行順序的情況。這也就要求了單個處理器的寫操作要馬上被其他處理器可見。
?
從上面的例子我們可以看到嚴格的順序一致性模型其實也是個概念模型,限制了編譯器的優化空間。實際的實現中編譯器做了大量的優化工作,這些優化工作的基礎就是指令重排序操作,而指令重排序打破了這種嚴格的順序一致性,比如單個處理器看到的指令執行順序可以和它的程序定義順序一致。
?
3. 因果一致性 Causal Consistency
因果一致性是一種弱的順序一致性,只有有因果關系的數據才需要保證順序一致性,沒有因果關系的數據不需要保證順序一致性,也就是說對于沒有因果關系的數據不需要其他處理器看到一致的視圖。
那么什么是因果關系呢?必須處理器A寫了一個x = a, W(x)a,處理器B先讀取x的值,再寫x的值,R(x)a, W(x)b,那么對處理器B來說,它的寫x操作和處理器A的寫x操作 就有因果關系,因為它后寫入的值可能依賴于處理器A先寫入的值。這時候這兩個操作要保持順序一致性,也就是說其他處理器看到的順序都是W(x)a, W(x)b。
實習因果一致性的實現復雜,需要額外的建立一個依賴關系圖,即一個操作依賴于其他什么操作。
?
4. 處理器一致性/ PRAM(Piplined RAM) 管道式存儲器
這兩種一致性經常被放在一起,概念基本一致,他們比因果一致性更弱,只要求從一個處理器來的寫操作按照同樣的順序被其他處理器看到,不同處理器的寫操作可以按照不同的順序被看到,也就是說它不保證有因果關系的寫操作按照執行的順序執行,拿上面因果一致性的例子來說,雖然W(x)a和W(x)b存在因果關系,但是對不同的處理器來說,它們可以先看到a也可以先看到b。
它的優點是同一個處理器的寫操作被管道化,相當于用管道串行了,并且隱藏了寫操作的延遲。比如一個處理器的寫操作可能還在寫緩存區沒有刷新到內存被其他處理器看到,這個處理器的讀操作可以馬上進行對這個變量的讀操作,而不需要等待它的寫操作完全被寫入內存,大大提高了系統的性能。不同處理器的寫操作是并行執行的
處理器一致性還是比較嚴格的一致性模型,因為同一個處理器的寫操作還是嚴格按照順序執行。而重排序的優化可以對沒有數據相關性的寫操作進行重排序。
?
上面這幾種一致性模型處理的問題域是對所有的共享變量而言,下面三種一致性模型是針對有明確定義的同步變量而言,可以理解為Java中的volatile變量,內置鎖的獲取/釋放
?
5. 弱一致性 Weak Consistency
弱一致性只對被同步操作保護的共享變量而言,規定了只有對共享變量的同步操作完成之后,共享數據才可能保持一致性.在同步操作過程中,是不保證一致性的,單個處理器對共享變量的修改對其他處理器是不可見的。相比與嚴格的順序一致性,它只保持了執行順序上的順序一致性,至于可見性必須要等待同步操作結束
- 對同步變量的讀寫按照順序一致性
- 只有所有對同步變量的寫操作完成之后才能對同步變量進行訪問
- 只有所有對同步變量的訪問(讀/寫)完成后才能對同步變量訪問
?
6. 釋放一致性 Release Consistency
弱一致性的粒度太大,包含了進入同步操作和釋放同步操作兩部分,而只有同步操作整體完成后,其他處理器才有可能保持一致性。 釋放一致性規定了對同步變量的釋放操作后,就對同步變量的狀態廣播到其他處理器
?
7. 進入一致性 Entry Consistency
和釋放一致性一樣,也是為了減小弱一致性的粒度,進入同步變量時,獲取同步變量的最新狀態
?
所以如果一個共享變量要被同步操作保護,那么所有操作它的地方都要被同步保護,否則就不保證一致性
?
8. 緩存一致性 Cache Consistency
緩存一致性的語義和上面的數據一致性模型有些區別,它主要說的是多個CPU緩存之間的一致性協議,我們要知道的是現代CPU基本都提供了緩存一致性的實現,比如一個CPU修改了一個緩存,那么其他CPU可以馬上看到修改的緩存數據。這篇文章說了下緩存一致性的內容聊聊高并發(五)理解緩存一致性協議以及對并發編程的影響
?
在分布式存儲領域的弱一致性,最終一致性的語義和并發編程里的一致性語義稍有差別,實際上事務隔離級別也是對數據一致性的不同需求,這些概念以后有機會說數據庫的時候再提。
?
在這篇文章聊聊高并發(十九)理解并發編程的幾種"性" -- 可見性,有序性,原子性?中我們說并發編程中的可見性和有序性,分析了上面的這么多一致性模型,我們可以看到順序一致性是嚴格保證了所有共享變量的可見性和有序性。深入理解Java內存模型(三)——順序一致性?的這張圖畫的很有意思,所有對共享變量的操作在順序一致性下被串行了
?
但是順序一致性的性能是很差的,而且實現起來很昂貴,一方面它限制了重排序來保證執行順序,另一方面它對所有的共享變量都要求保證可見性,來使所有CPU看到一直的執行視圖。
現代的計算機系統提供了大量的優化操作,比如多個階段的指令重排序,松散的內存模型。一般性能越高的機器,提供的內存模型越發地松散,比如允許各種情況的重排序,從而提高優化的空間。而常用的X86架構,只允許“寫后讀”的重排序。
?
Java內存模型是構建在這些底層內存模型上的語言級的內存模型,它要屏蔽底層各種內存模型的差異性,提供一致的對上層應用的一致性試圖。另一方面它又要支持多種級別的優化操作,所以實際執行的程序和寫的Java代碼是完全不一樣的順序。
?
對于沒有同步的共享變量的操作,Java內存模型只保證從單線程執行的角度來說,程序的執行結果和程序定義的結果是一致性的,只保證正確性,但是不保證執行的順序性,因為沒有數據相關性的代碼是可以重排序的。
?
對于提供了同步的共享變量的操作,Java內存模型保證了弱一致性 / 釋放一致性 / 進入一致性,通過加內存屏障(Memory Barrier)實現。
- 讀volatile變量,進入鎖,都會保證進入一致性,刷新CPU緩存,保證數據的可見性。
- 寫volatile變量,釋放鎖,都會保證釋放一致性,把寫緩存區數據刷新到內存,保證數據的可見性。
- Java內存模型還保證了同步變量的順序性,對于同步變量的操作不允許進行指令重排序,比如鎖臨界區的數據逸出到臨界區外,volatile的寫操作被重排序到前面(JDK1.5之前volatile沒有防止重排序的語義,導致雙重檢查加鎖的單實例模式實際是失效的)
Java內存模型還支持一組Happens-Before定義的偏序關系,后面會專門說說Happens-Before
?
結論是Java內存模型是一種松散的語言級內存模型,提供了給編譯器充分優化的空間,它只對顯式同步的共享數據提供弱一致性支持,比如volatile變量,內置鎖,顯式鎖,各種同步器,要記住的是這些被同步手段保護的共享數據在語義上是有順序一致性的,防止重排序保證了它們在單個線程的順序性,內存屏障又保證了它們在多個線程的可見性和順序性
?
在一致性這個問題域中,各個層面扮演的角色大致如下:
1. 一致性模型,定義了各種一致性模型的理論基礎
2. 硬件層,提供了實現某些一致性模型的硬件能力。硬件在默認情況下按照最基本的方式運行,比如
- 對同一個線程沒有數據依賴的指令可以重排序優化執行,有數據依賴的指令按照程序順序執行,從而保證單線程程序運行的正確性
- 保證讀操作讀到的數據肯定是之前在同一位置寫入的數據
3. 語言層,少數語言提供了語言層面的滿足一致性模型的編程能力,另外一些語言則直接使用硬件層提供了一致性編程的能力。提供一致性能力語言的工作方式如下:
- 把滿足一致性需求的編程能力作為一種資源,指定一些規則,比如volitile, synchronized,Happens-before規則等
- 當應用層需要使用這種編程能力的時候,需要顯式地提出申請,比如顯式地使用volatile來標識變量
- 通過編譯器適配底層各種硬件平臺提供了一致性編程的能力,比如有些平臺使用內存屏障,有些平臺使用read-modified-write,需要語言層來屏蔽這種差異性
?
4. 應用層,比如分布式系統,比如并發的服務器程序,它們在一致性問題中的工作有
- 根據實際需求來定義應用所需要滿足的一致性需求
- 定義和選擇相應的實現一致性需求的算法,比如分布式存儲中通過消息協議實現的Paxos,Zab,多階段提交等
- 利用編程語言提供了基本的一致性編程的能力作為實現一致性需求算法的基礎
?
從這個層面上理解, Java內存模型主要做了幾件事情
1. 適配各種底層硬件平臺提供的一致性編程能力,比如加一個內存屏障,在不同平臺下要加的內存屏障的數量,順序可能不同
2. 定義Happens-before規則
3. 定義了各種語言級提供一致性能力的語法,比如volatile, final, synchronized, 顯式鎖,各種同步器,Unsafe對CAS操作的封裝等等
?
?
參考資料:
Consistency Model
深入理解Java內存模型(三)——順序一致性
為什么程序員需要關心順序一致性(Sequential Consistency)而不是Cache一致性(Cache Coherence)
存儲一致性總結
總結
以上是生活随笔為你收集整理的聊聊高并发(三十三)Java内存模型那些事(一)从一致性(Consistency)的角度理解Java内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊高并发(三十一)解析java.uti
- 下一篇: 聊聊高并发(三十五)Java内存模型那些