【教程】終於有人把Java記憶體模型說清楚了!

陶然陶然發表於2018-08-02

網上有很多關於 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),二級快取(L2),部分高階 CPU 還具有三級快取(L3),每一級快取中所儲存的全部資料都是下一級快取的一部分。

這三種快取的技術難度和製造成本是相對遞減的,所以其容量也是相對遞增的。

那麼,在有了多級快取之後,程式的執行就變成了:當 CPU 要讀取一個資料時,首先從一級快取中查詢,如果沒有找到再從二級快取中查詢,如果還是沒有就從三級快取或記憶體中查詢。

隨著公司越來越大,老闆要管的事情越來越多,公司的管理部門開始改革,開始出現高層,中層,底層等管理者。一級一級之間逐層管理。

單核 CPU 只含有一套 L1,L2,L3 快取;如果 CPU 含有多個核心,即多核 CPU,則每個核心都含有一套 L1(甚至和 L2)快取,而共享 L3(或者和 L2)快取。

公司也分很多種,有些公司只有一個大 Boss,他一個人說了算。但是有些公司有比如聯席總經理、合夥人等機制。

單核 CPU 就像一家公司只有一個老闆,所有命令都來自於他,那麼就只需要一套管理班底就夠了。

多核 CPU 就像一家公司是由多個合夥人共同創辦的,那麼,就需要給每個合夥人都設立一套供自己直接領導的高層管理人員,多個合夥人共享使用的是公司的底層員工。

還有的公司,不斷壯大,開始拆分出各個子公司。各個子公司就是多個 CPU 了,互相之前沒有共用的資源。互不影響。

下圖為一個單 CPU 雙核的快取結構:

隨著計算機能力不斷提升,開始支援多執行緒。那麼問題就來了,我們分別來分析下單執行緒、多執行緒在單核 CPU、多核 CPU 中的影響。

單執行緒:CPU 核心的快取只被一個執行緒訪問。快取獨佔,不會出現訪問衝突等問題。

單核 CPU,多執行緒:程式中的多個執行緒會同時訪問程式中的共享資料,CPU 將某塊記憶體載入到快取後,不同執行緒在訪問相同的實體地址的時候,都會對映到相同的快取位置,這樣即使發生執行緒的切換,快取仍然不會失效。

但由於任何時刻只能有一個執行緒在執行,因此不會出現快取訪問衝突。

多核 CPU,多執行緒:每個核都至少有一個 L1  快取。多個執行緒訪問程式中的某個共享記憶體,且這多個執行緒分別在不同的核心上執行,則每個核心都會在各自的 Cache 中保留一份共享記憶體的緩衝。

由於多核是可以並行的,可能會出現多個執行緒同時寫各自的快取的情況,而各自的 Cache 之間的資料就有可能不同。

在 CPU 和主存之間增加快取,在多執行緒場景下就可能存在快取一致性問題,也就是說,在多核 CPU 中,每個核的自己的快取中,關於同一個資料的快取內容可能不一致。

如果這家公司的命令都是序列下發的話,那麼就沒有任何問題。

如果這家公司的命令都是並行下發的話,並且這些命令都是由同一個 CEO 下發的,這種機制是也沒有什麼問題。因為他的命令執行者只有一套管理體系。

如果這家公司的命令都是並行下發的話,並且這些命令是由多個合夥人下發的,這就有問題了。

因為每個合夥人只會把命令下達給自己直屬的管理人員,而多個管理人員管理的底層員工可能是公用的。

比如,合夥人 1 要辭退員工 a,合夥人 2 要給員工 a 升職,升職後的話他再被辭退需要多個合夥人開會決議。兩個合夥人分別把命令下發給了自己的管理人員。

合夥人 1 命令下達後,管理人員 a 在辭退了員工後,他就知道這個員工被開除了。

而合夥人 2 的管理人員 2 這時候在沒得到訊息之前,還認為員工 a 是在職的,他就欣然的接收了合夥人給他的升職 a 的命令。

處理器優化和指令重排

上面提到在 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 就作用於工作記憶體和主存之間資料同步過程。它規定了如何做資料同步以及什麼時候做資料同步。

這裡面提到的主記憶體和工作記憶體,讀者可以簡單的類比成計算機記憶體模型中的主存和快取的概念。

特別需要注意的是,主記憶體和工作記憶體與 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 中記憶體模型做了什麼事情等。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28285180/viewspace-2168992/,如需轉載,請註明出處,否則將追究法律責任。

相關文章