網上有很多關于Java記憶體模型的文章,在《深入了解Java虛拟機》和《Java并發程式設計的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之後還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的介紹一下Java記憶體模型,目的很簡單,讓你讀完本文以後,就知道到底Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題等。
為什麼要有記憶體模型
在介紹Java記憶體模型之前,先來看一下到底什麼是計算機記憶體模型,然後再來看Java記憶體模型在計算機記憶體模型的基礎上做了哪些事情。要說計算機的記憶體模型,就要說一下一段古老的曆史,看一下為什麼要有記憶體模型。
記憶體模型,英文名Memory Model,他是一個很老的老古董了。他是與計算機硬體有關的一個概念。那麼我先給你介紹下他和硬體到底有啥關系。
CPU和緩存一緻性
我們應該都知道,計算機在執行程式的時候,每條指令都是在CPU中執行的,而執行的時候,又免不了要和資料打交道。而計算機上面的資料,是存放在主存當中的,也就是計算機的實體記憶體啦。
剛開始,還相安無事的,但是随着CPU技術的發展,CPU的執行速度越來越快。而由于記憶體的技術并沒有太大的變化,是以從記憶體中讀取和寫入資料的過程和CPU的執行速度比起來差距就會越來越大,這就導緻CPU每次操作記憶體都要耗費很多等待時間。
這就像一家創業公司,剛開始,創始人和員工之間工作關系其樂融融,但是随着創始人的能力和野心越來越大,逐漸和員工之間出現了差距,普通員工原來越跟不上CEO的腳步。老闆的每一個指令,傳到到基層員工之後,由于基層員工的了解能力、執行能力的欠缺,就會耗費很多時間。這也就無形中拖慢了整家公司的工作效率。
可是,不能因為記憶體的讀寫速度慢,就不發展CPU技術了吧,總不能讓記憶體成為計算機處理的瓶頸吧。
是以,人們想出來了一個好的辦法,就是在CPU和記憶體之間增加高速緩存。緩存的概念大家都知道,就是儲存一份資料拷貝。他的特點是速度快,記憶體小,并且昂貴。
那麼,程式的執行過程就變成了:
當程式在運作過程中,會将運算需要的資料從主存複制一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取資料和向其中寫入資料,當運算結束之後,再将高速緩存中的資料重新整理到主存當中。
之後,這家公司開始設立中層管理人員,管理人員直接歸CEO上司,上司有什麼訓示,直接告訴管理人員,然後就可以去做自己的事情了。管理人員負責去協調底層員工的工作。因為管理人員是了解手下的人員以及自己負責的事情的。是以,大多數時候,公司的各種決策,通知等,CEO隻要和管理人員之間溝通就夠了。
而随着CPU能力的不斷提升,一層緩存就慢慢的無法滿足要求了,就逐漸的衍生出多級緩存。
按照資料讀取順序和與CPU結合的緊密程度,CPU緩存可以分為一級緩存(L1),二級緩存(L3),部分高端CPU還具有三級緩存(L3),每一級緩存中所儲存的全部資料都是下一級緩存的一部分。
這三種緩存的技術難度和制造成本是相對遞減的,是以其容量也是相對遞增的。
那麼,在有了多級緩存之後,程式的執行就變成了:
當CPU要讀取一個資料時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或記憶體中查找。
随着公司越來越大,老闆要管的事情越來越多,公司的管理部門開始改革,開始出現高層,中層,底層等管理者。一級一級之間逐層管理。
單核CPU隻含有一套L1,L2,L3緩存;如果CPU含有多個核心,即多核CPU,則每個核心都含有一套L1(甚至和L2)緩存,而共享L3(或者和L2)緩存。
公司也分很多種,有些公司隻有一個大Boss,他一個人說了算。但是有些公司有比如聯席總經理、合夥人等機制。
單核CPU就像一家公司隻有一個老闆,所有指令都來自于他,那麼就隻需要一套管理班底就夠了。
多核CPU就像一家公司是由多個合夥人共同創辦的,那麼,就需要給每個合夥人都設立一套供自己直接上司的高層管理人員,多個合夥人共享使用的是公司的底層員工。
還有的公司,不斷壯大,開始差分出各個子公司。各個子公司就是多個CPU了,互相之前沒有共用的資源。互不影響。
下圖為一個單CPU雙核的緩存結構。

CACHE
随着計算機能力不斷提升,開始支援多線程。那麼問題就來了。我們分别來分析下單線程、多線程在單核CPU、多核CPU中的影響。
單線程。cpu核心的緩存隻被一個線程通路。緩存獨占,不會出現通路沖突等問題。
單核CPU,多線程。程序中的多個線程會同時通路程序中的共享資料,CPU将某塊記憶體加載到緩存後,不同線程在通路相同的實體位址的時候,都會映射到相同的緩存位置,這樣即使發生線程的切換,緩存仍然不會失效。但由于任何時刻隻能有一個線程在執行,是以不會出現緩存通路沖突。
多核CPU,多線程。每個核都至少有一個L1 緩存。多個線程通路程序中的某個共享記憶體,且這多個線程分别在不同的核心上執行,則每個核心都會在各自的caehe中保留一份共享記憶體的緩沖。由于多核是可以并行的,可能會出現多個線程同時寫各自的緩存的情況,而各自的cache之間的資料就有可能不同。
在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一緻性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關于同一個資料的緩存内容可能不一緻。
如果這家公司的指令都是串行下發的話,那麼就沒有任何問題。
如果這家公司的指令都是并行下發的話,并且這些指令都是由同一個CEO下發的,這種機制是也沒有什麼問題。因為他的指令執行者隻有一套管理體系。
如果這家公司的指令都是并行下發的話,并且這些指令是由多個合夥人下發的,這就有問題了。因為每個合夥人隻會把指令下達給自己直屬的管理人員,而多個管理人員管理的底層員工可能是公用的。
比如,合夥人1要辭退員工a,合夥人2要給員工a升職,升職後的話他再被辭退需要多個合夥人開會決議。兩個合夥人分别把指令下發給了自己的管理人員。合夥人1指令下達後,管理人員a在辭退了員工後,他就知道這個員工被開除了。而合夥人2的管理人員2這時候在沒得到消息之前,還認為員工a是在職的,他就欣然的接收了合夥人給他的升職a的指令。
image.png
處理器優化和指令重排
上面提到在在CPU和主存之間增加緩存,在多線程場景下會存在緩存一緻性問題。除了這種情況,還有一種硬體問題也比較重要。那就是為了使處理器内部的運算單元能夠盡量的被充分利用,處理器可能會對輸入代碼進行亂序執行處理。這就是處理器優化。
除了現在很多流行的處理器會對代碼進行優化亂序處理,很多程式設計語言的編譯器也會有類似的優化,比如Java虛拟機的即時編譯器(JIT)也會做指令重排。
可想而知,如果任由處理器優化和編譯器對指令重排的話,就可能導緻各種各樣的問題。
關于員工組織調整的情況,如果允許人事部在接到多個指令後進行随意拆分亂序執行或者重排的話,那麼對于這個員工以及這家公司的影響是非常大的。
并發程式設計的問題
前面說的和硬體有關的概念你可能聽得有點蒙,還不知道他到底和軟體有啥關系。但是關于并發程式設計的問題你應該有所了解,比如原子性問題,可見性問題和有序性問題。
其實,原子性問題,可見性問題和有序性問題。是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的緩存一緻性問題、處理器優化問題和指令重排問題等。
這裡簡單回顧下這三個問題,并不準備深入展開,感興趣的讀者可以自行學習。我們說,并發程式設計,為了保證資料的安全,需要滿足以下三個特性:
原子性是指在一個操作中就是cpu不可以在中途暫停然後再排程,既不被中斷操作,要不執行完成,要不就不執行。
可見性是指當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性即程式執行的順序按照代碼的先後順序執行。
有沒有發現,緩存一緻性問題其實就是可見性問題。而處理器優化是可以導緻原子性問題的。指令重排即會導緻有序性問題。是以,後文将不再提起硬體層面的那些概念,而是直接使用大家熟悉的原子性、可見性和有序性。
什麼是記憶體模型
前面提到的,緩存一緻性問題、處理器器優化的指令重排問題是硬體的不斷更新導緻的。那麼,有沒有什麼機制可以很好的解決上面的這些問題呢?
最簡單直接的做法就是廢除處理器和處理器的優化技術、廢除CPU緩存,讓CPU直接和主存互動。但是,這麼做雖然可以保證多線程下的并發問題。但是,這就有點因噎廢食了。
是以,為了保證并發程式設計中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——記憶體模型。
為了保證共享記憶體的正确性(可見性、有序性、原子性),記憶體模型定義了共享記憶體系統中多線程程式讀寫操作行為的規範。通過這些規則來規範對記憶體的讀寫操作,進而保證指令執行的正确性。它與處理器有關、與緩存有關、與并發有關、與編譯器也有關。他解決了CPU多級緩存、處理器優化、指令重排等導緻的記憶體通路問題,保證了并發場景下的一緻性、原子性和有序性。
記憶體模型解決并發問題主要采用兩種方式:限制處理器優化和使用記憶體屏障。本文就不深入底層原理來展開介紹了,感興趣的朋友可以自行學習。
什麼是Java記憶體模型
前面介紹過了計算機記憶體模型,這是解決多線程場景下并發問題的一個重要規範。那麼具體的實作是如何的呢,不同的程式設計語言,在實作上可能有所不同。
我們知道,Java程式是需要運作在Java虛拟機上面的,Java記憶體模型(Java Memory Model ,JMM)就是一種符合記憶體模型規範的,屏蔽了各種硬體和作業系統的通路差異的,保證了Java程式在各種平台下對記憶體的通路都能保證效果一緻的機制及規範。
提到Java記憶體模型,一般指的是JDK 5 開始使用的新的記憶體模型,主要由
JSR-133: JavaTM Memory Model and Thread Specification描述。感興趣的可以參看下這份PDF文檔(
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf)
Java記憶體模型規定了所有的變量都存儲在主記憶體中,每條線程還有自己的工作記憶體,線程的工作記憶體中儲存了該線程中是用到的變量的主記憶體副本拷貝,線程對變量的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。不同的線程之間也無法直接通路對方工作記憶體中的變量,線程間變量的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。
而JMM就作用于工作記憶體和主存之間資料同步過程。他規定了如何做資料同步以及什麼時候做資料同步。
JAVA
這裡面提到的主記憶體和工作記憶體,讀者可以簡單的類比成計算機記憶體模型中的主存和緩存的概念。特别需要注意的是,主記憶體和工作記憶體與JVM記憶體結構中的Java堆、棧、方法區等并不是同一個層次的記憶體劃分,無法直接類比。《深入了解Java虛拟機》中認為,如果一定要勉強對應起來的話,從變量、主記憶體、工作記憶體的定義來看,主記憶體主要對應于Java堆中的對象執行個體資料部分。工作記憶體則對應于虛拟機棧中的部分區域。
是以,再來總結下,JMM是一種規範,目的是解決由于多線程通過共享記憶體進行通信時,存在的本地記憶體資料不一緻、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題。
Java記憶體模型的實作
了解Java多線程的朋友都知道,在Java中提供了一系列和并發處理相關的關鍵字,比如
volatile
、
synchronized
final
concurren
包等。其實這些就是Java記憶體模型封裝了底層的實作後提供給程式員使用的一些關鍵字。
在開發多線程的代碼的時候,我們可以直接使用
synchronized
等關鍵字來控制并發,從來就不需要關心底層的編譯器優化、緩存一緻性等問題。是以,Java記憶體模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實作後,供開發者直接使用。
本文并不準備把所有的關鍵字逐一介紹其用法,因為關于各個關鍵字的用法,網上有很多資料。讀者可以自行學習。本文還有一個重點要介紹的就是,我們前面提到,并發程式設計要解決原子性、有序性和一緻性的問題,我們就再來看下,在Java中,分别使用什麼方式來保證。
原子性
在Java中,為了保證原子性,提供了兩個進階的位元組碼指令
monitorenter
和
monitorexit
。在
synchronized的實作原理文章中,介紹過,這兩個位元組碼,在Java中對應的關鍵字就是
synchronized
。
是以,在Java中可以使用
synchronized
來保證方法和代碼塊内的操作是原子性的。
可見性
Java記憶體模型是通過在變量修改後将新值同步回主記憶體,在變量讀取前從主記憶體重新整理變量值的這種依賴主記憶體作為傳遞媒介的方式來實作的。
Java中的
volatile
關鍵字提供了一個功能,那就是被其修飾的變量在被修改後可以立即同步到主記憶體,被其修飾的變量在每次是用之前都從主記憶體重新整理。是以,可以使用
volatile
來保證多線程操作時變量的可見性。
除了
volatile
,Java中的
synchronized
final
兩個關鍵字也可以實作可見性。隻不過實作方式不同,這裡不再展開了。
有序性
在Java中,可以使用
synchronized
volatile
來保證多線程之間操作的有序性。實作方式有所差別:
volatile
關鍵字會禁止指令重排。
synchronized
關鍵字保證同一時刻隻允許一條線程操作。
好了,這裡簡單的介紹完了Java并發程式設計中解決原子性、可見性以及有序性可以使用的關鍵字。讀者可能發現了,好像
synchronized
關鍵字是萬能的,他可以同時滿足以上三種特性,這其實也是很多人濫用
synchronized
的原因。
但是
synchronized
是比較影響性能的,雖然編譯器提供了很多鎖優化技術,但是也不建議過度使用。
總結
在讀完本文之後,相信你應該了解了什麼是Java記憶體模型、Java記憶體模型的作用以及Java中記憶體模型做了什麼事情等。關于Java中這些和記憶體模型有關的關鍵字,希望讀者還可以繼續深入學習,并且自己寫幾個例子親自體會一下。
可以參考《深入了解Java虛拟機》和《Java并發程式設計的藝術》兩本書。
轉載
http://www.hollischuang.com/archives/2550