前言在學習java并發編程的過程中,我們通常會遇到一個概念,那就是“java內存模型”。在我之前的很長一段時間里,對于“java內存模型”都是處于一種似懂非懂的朦朧狀態,看似理解了與之相關的緩存一致性,原子性,可見性,有序性,happen-before原則,內存屏障 等一系列概念,但是總是無法把他們串聯起來,還原java內存模型的本來面貌。
相關概念定義首先,在這里,我們先對內存模型做一個總的抽象的定義: 內存模型本質上是對內存訪問(讀取和寫入)的規則、規范、以及協議 的一種抽象,這個抽象分為主要分為兩個層面:1.內存的結構、2.內存的訪問(讀取和寫入)規則。 內存模型從不同的層次去看,他可以分為處理器的內存模型、操作系統的內存模型、編程語言的內存模型(如java內存模型)、應用的內存模型(如redis的內存模型)。本質上他們都只是站在不同的角度,對內存的讀寫、管理、結構定義了抽象的規范。
處理器內存模型 在談java內存模型之前,有必要先談談處理器的內存模型。因為java內存模型是一種高層次的抽象,本質上它的實現也是建立在物理硬件的內存模型和操作系統的內存模型之上的,適當的了解底層的內存模型的接口,可以幫助我們更好的理解java內存模型。 從個硬件性能的角度講,由于CPU的運算速度遠大于內存的讀寫速度,如果CPU進行運算時,每次都從內存中讀寫數據,那么CPU將會有大量的時間是在等待內存的數據讀寫,這將會導致CPU利用率低下,所以為了最大化CPU的性能和最優化的成本考慮,現代計算機在CPU和內存之間增加了多個高速緩存(L1>L2>L3>Memory),通過將運算需要使用到的數據復制到高速緩存中,當CPU運算時直接從高速緩存讀寫數據,當運算結束后再從緩存把數據同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。由此實現了,在成本可控的前提下(如果使用制作高速緩沖區的材料來制作內存,成本過高,經濟效益低下),最大化發揮CPU的性能。 現代常見的多核CPU為多級緩存結構,一般為三級緩存,即L1 cache,L2 cache,L3 cache,存取性能依次遞減,具體可參照下圖。其中L1為CPU core(cpu核心)私有,即每個cpu core都有一個專屬于自己的L1 cache,而L2,L3為多個CPU core共享。
雖然這種基于高速緩存的方式,提高了CPU的使用效率,解決了內存讀取速度與CPU運行速度的矛盾,但同時也引入了新的復雜度,那就是緩存一致性問題。簡單的說,緩存一致性問題就是當多個處理器對同一變量進行并發操作的時候,由于每個CPU都有各自的高速緩存,那最后寫入主存的數據該以誰的緩存為準呢?可以想見,如果沒有協議規范這種內存讀寫操作,那么程序在并發運行情況就會充滿不確定性,從而無法正常運行。
為了解決上述問題,現代CPU在高速緩存與主內存之間引入了“緩存一致性協議”來解決這個問題,其具體結構如下圖:
下面我們簡單的介紹一下比較常見的緩存一致性協議“MESI協議”,它的核心思想就是通過給緩存行(cpu從內存加載數據到高速緩存的基本單位)添加一系列狀態(具體狀態如下表),而后限制CPU對不同狀態的緩存行的讀寫操作,從而達到數據一致性的目的。
java內存模型眾所周知java是一門“跨平臺”的編程語言,它的核心設計目標是“一次編譯,到處運行”,然而在java之前的主流編程語言(如c/c++等),對于內存的訪問操作使用的是物理硬件和操作系統的內存模型。因此,會由于不同平臺上內存模型的差異,有可能會導致程序在一套平臺上并發完成正常,而在另外一套平臺上并發訪問卻經常出現錯誤。由上可以看出,直接使用物理硬件和操作系統的內存模型的設計與java語言“一次編譯,到處運行”的設計目標是相違背。因此,java語言的設計者,設計了java內存模型,一組java虛擬機級別的抽象的內存訪問規范。
通過上面的介紹,我們可以了解到定義java內存模型的目的,是為了隔離不同硬件和操作系統上內存訪問的差異,給java開發者提供一致的內存訪問模型,從而實現java語言的跨平臺性。某種意義上講java內存模型其本質就是位于java開發者與硬件、操作系統內存模型之間的一組接口,而java內存模型的具體實現則像是一個適配器,使得同一java程序可以在各個平臺上實現安全的并發內存訪問。 從jvm的設計者角度看,定義java內存模型并非是一件容易的事情,一方面它需要足夠嚴謹,這樣才能讓java的并發內存訪問不產生歧義,另一方面,它又需要足夠的寬松,給jvm的實現者留下可能多的優化空間。
jvm內存操作的字節碼指令
內存讀寫的亂序優化 如果要把一個變量從主內存復制到工作內存,那就要順序地執行read和load操作,如果要把變量從工作內存同步回主內存,就要順序地執行store和write操作。但是,Java內存模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其他指令的,如對主內存中的變量a、b進行訪問時,一種可能出現順序是read a、read b、load b、load a。
并發讀寫下的可見性問題 happen-before原則
讀。
happens-before于線程A從ThreadB.join()操作成功返回。
happen-before原則,描述的是一種偏序關系,并不是簡單的時序上的先后關系,而是對內存讀寫順序的保證。用個更加易于理解的詞來描述,那就是“可見性”,即A happen-before B 說明的是A操作的操作結果對于B操作可見。
|