在介紹Java記憶體模型之前,先來看下計算機的記憶體模型,然後再來看Java記憶體模型在計算機的記憶體模型基礎上做了哪些事情。
要說計算機的記憶體模型,首先來看下為何要有記憶體模型?
一、為什麼要有記憶體模型
首先,“記憶體模型”是一個與計算機硬體有關的概念,先來看下有什麼關係?
CPU和快取一致性
記憶體速度滿足不了CPU的讀寫速度:計算機在執行程式的時候,每條指令都是在CPU中執行的,且免不了要和資料打交道,而計算機上面的資料,是存放在主存當中的,也就是計算機的實體記憶體啦。剛開始,還相安無事的,但是隨著CPU技術的發展,CPU的執行速度越來越快。而由於記憶體的技術並沒有太大的變化,所以從記憶體中讀取和寫入資料的過程和CPU的執行速度比起來差距就會越來越大,這就導致CPU每次操作記憶體都要耗費很多等待時間。
CPU和記憶體之間增加快取記憶體:可是,不能因為記憶體的讀寫速度慢,就不發展CPU技術,總不能讓記憶體成為計算機處理的瓶頸吧。所以,人們想出來了一個好的辦法,就是在CPU和記憶體之間增加快取記憶體。快取的概念大家都知道,就是儲存一份資料拷貝。他的特點是速度快,記憶體小,並且昂貴。那麼,程式的執行過程就變成了:當程式在執行過程中,會將運算需要的資料從主存複製一份到CPU的快取記憶體當中,那麼CPU進行計算時就可以直接從它的快取記憶體讀取資料和向其中寫入資料,當運算結束之後,再將快取記憶體中的資料重新整理到主存當中。
衍生出多級快取:而隨著CPU能力的不斷提升,一層快取就慢慢的無法滿足要求了,就逐漸的衍生出多級快取。按照資料讀取順序和與CPU結合的緊密程度,CPU快取可以分為一級快取(L1),二級快取(L3),部分高階CPU還具有三級快取(L3),每一級快取中所儲存的全部資料都是下一級快取的一部分。這三種快取的技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。那麼,在有了多級快取之後,程式的執行就變成了:當CPU要讀取一個資料時,首先從一級快取中查詢,如果沒有找到再從二級快取中查詢,如果還是沒有就從三級快取或記憶體中查詢。
多執行緒、多CPU出現問題:單核CPU只含有一套L1,L2,L3快取;如果CPU含有多個核心,即多核CPU,則每個核心都含有一套L1(甚至和L2)快取,而共享L3(或者和L2)快取。
單執行緒、單CPU核心
cpu核心的快取只被一個執行緒訪問。快取獨佔,不會出現訪問衝突等問題。
多執行緒、單CPU核心
程式中的多個執行緒會同時訪問程式中的共享資料,CPU將某塊記憶體載入到快取後,不同執行緒在訪問相同的實體地址的時候,都會對映到相同的快取位置,這樣即使發生執行緒的切換,快取仍然不會失效。但由於任何時刻只能有一個執行緒在執行,因此不會出現快取訪問衝突。
多執行緒、多CPU核心
每個核都至少有一個L1 快取。多個執行緒訪問程式中的某個共享記憶體,且這多個執行緒分別在不同的核心上執行,則每個核心都會在各自的caehe中保留一份共享記憶體的緩衝。由於多核是可以並行的,可能會出現多個執行緒同時寫各自的快取的情況,而各自的cache之間的資料就有可能不同。
在CPU和主存之間增加快取,在多執行緒場景下就可能存在快取一致性問題,也就是說,在多核CPU中,每個核的自己的快取中,關於同一個資料的快取內容可能不一致。
處理器優化和指令重排
上面提到在在CPU和主存之間增加快取,在多執行緒場景下會存在快取一致性問題。除了這種情況,還有一種硬體問題也比較重要。那就是為了使處理器內部的運算單元能夠儘量的被充分利用,處理器可能會對輸入程式碼進行亂序執行處理。這就是處理器優化。除了現在很多流行的處理器會對程式碼進行優化亂序處理,很多程式語言的編譯器也會有類似的優化,比如Java虛擬機器的即時編譯器(JIT)也會做指令重排。
可想而知,如果任由處理器優化和編譯器對指令重排的話,就可能導致各種各樣的問題。
二、併發程式設計的問題
併發程式設計存在的問題:原子性問題、可見性問題、有序性問題。這是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的快取一致性問題、處理器優化問題和指令重排問題等。
併發程式設計,為了保證資料的安全,需要滿足以下三個特性:
原子性:指在一個操作中就是cpu不可以在中途暫停然後再排程,既不被中斷操作,要不執行完成,要不就不執行
可見性:指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
有序性:程式執行的順序按照程式碼的先後順序執行。
不難發現,快取一致性問題其實就是可見性問題。而處理器優化是可以導致原子性問題的。指令重排即會導致有序性問題。
三、什麼是記憶體模型
快取一致性問題、處理器器優化的指令重排問題是硬體的不斷升級導致的。那麼,有沒有什麼機制可以很好的解決上面的這些問題呢?
最簡單直接的做法就是廢除處理器和處理器的優化技術、廢除CPU快取,讓CPU直接和主存互動。但是,這麼做雖然可以保證多執行緒下的併發問題。但是,這就有點因噎廢食了。
所以,為了保證併發程式設計中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——記憶體模型。
定義
為了保證共享記憶體的正確性(可見性、有序性、原子性),記憶體模型定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範。
目的
解決CPU多級快取、處理器優化、指令重排等導致的記憶體訪問問題,保證併發場景下的一致性、原子性和有序性。
四、Java記憶體模型
前面介紹過了計算機記憶體模型,這是解決多執行緒場景下併發問題的一個重要規範。那麼具體的實現是如何的呢,不同的程式語言,在實現上可能有所不同。
定義
Java程式是需要執行在Java虛擬機器上面的,Java記憶體模型(Java Memory Model ,JMM)就是一種符合記憶體模型規範的,遮蔽了各種硬體和作業系統的訪問差異的,保證了Java程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。
Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒中是用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。而JMM就作用於工作記憶體和主存之間資料同步過程。他規定了如何做資料同步以及什麼時候做資料同步。
主記憶體和工作記憶體
可以簡單的類比成計算機記憶體模型中的主存和快取的概念。特別需要注意的是,主記憶體和工作記憶體與JVM記憶體結構中的Java堆、棧、方法區等並不是同一個層次的記憶體劃分,無法直接類比。《深入理解Java虛擬機器》中認為,如果一定要勉強對應起來的話,從變數、主記憶體、工作記憶體的定義來看,主記憶體主要對應於Java堆中的物件例項資料部分。工作記憶體則對應於虛擬機器棧中的部分割槽域。
JMM
是一種規範,目的是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。
Java記憶體模型的實現
在Java中提供了一系列和併發處理相關的關鍵字,比如volatile、synchronized、final、concurren包等。其實這些就是Java記憶體模型封裝了底層的實現後提供給程式設計師使用的一些關鍵字。
原子性
在Java中,為了保證原子性,提供了兩個高階的位元組碼指令monitorenter和monitorexit。這兩個位元組碼,在Java中對應的關鍵字就是synchronized。因此,在Java中可以使用synchronized來保證方法和程式碼塊內的操作是原子性的。
可見性
Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值的這種依賴主記憶體作為傳遞媒介的方式來實現的。Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變數在被修改後可以立即同步到主記憶體,被其修飾的變數在每次是用之前都從主記憶體重新整理。因此,可以使用volatile來保證多執行緒操作時變數的可見性。除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性。只不過實現方式不同,這裡不再展開了。
有序性
在Java中,可以使用synchronized和volatile來保證多執行緒之間操作的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條執行緒操作。
通過以上可以發現,好像synchronized關鍵字是萬能的,他可以同時滿足以上三種特性,這其實也是很多人濫用synchronized的原因。但是synchronized是比較影響效能的,雖然編譯器提供了很多鎖優化技術,但是也不建議過度使用。