1. 程式人生 > 程式設計 >深入分析java記憶體模型(注意和java記憶體結構的區別)

深入分析java記憶體模型(注意和java記憶體結構的區別)

最近在更java多執行緒相關的文章,正好有人問我一些java記憶體模型的問題,因此花了一些時間,好好地瞭解一下。本篇文章主要是為瞭解決以下幾個問題?

1、java記憶體模型和java記憶體結構有什麼區別?

2、為什麼要有記憶體模型?

3、java的記憶體模型是什麼樣子的?

這篇文章,基本上不會涉及到程式碼,全是一些概念性的知識,但是也是面試常問和java進階所需要掌握的必要的基本知識點,所以,希望你耐著性子,慢慢來。

一、java記憶體模型和java記憶體結構有什麼區別

1、java記憶體結構

記得是在好幾年前研究Android的時候,看的java記憶體模型,時常和java記憶體結構分不清,因此,這一小節是針對小白或者是對其概念還不理解的人。

我們都知道,我們的java程式碼其實是不能直接執行的,他要經過一系列的步驟。看下圖:

我們的java檔案,首先要經過程式設計成為class檔案,然後通過類裝載器載入到jvm中去執行。這個jvm(紅色虛線框起來的這部分)就是java執行時資料區,意思就是java程式碼在執行的時候,這些資料要存放在不同的記憶體空間裡面。jvm就是指代這個的。當然了上面的執行時資料區jvm是jdk1.7版本的。也就是說不同的jdk版本,這個jvm長得是不一樣的。我們可以把java7的記憶體結構拿出來:

我們可以看到一共劃分了5個部分,其中java堆區和方法區還是所有執行緒共享的一片區域。為什麼要所有執行緒共享呢?因為假設一個資料,每個執行緒都保留一份,那其中有一個執行緒調皮,把這個資料更改了。其他的執行緒發現自己的資料沒有變,這就出現了問題了。於是設計成了所有執行緒共享,java記憶體模型出來了。

2、java記憶體模型

java記憶體模型也叫做JMM,但是這個模型可不是像java記憶體結構一樣,是真實存在的。java記憶體模型是一個抽象出來的概念。意思是把一部分記憶體區域設計成所有執行緒共享的,一個執行緒對資料更改,其他執行緒就能立刻知道。這種設計的方法叫做記憶體模型。我們可以提前看一下:java記憶體模型長什麼樣。

這就是java記憶體模型,也就是多個執行緒共享同一份資料。現在不知道你理解java記憶體模型和java記憶體結構的區別了沒有,我們可以這樣來總結一下:

(1)java記憶體結構是解決java中的資料如何存放的問題。

(2)java記憶體模型是解決java中多個執行緒共享資料的問題。

OK,到了這基本上就算是把兩者的區別介紹完了。下面就來看看為什麼要有記憶體模型吧

二、為什麼要有記憶體模型

深入理解java虛擬機器器是從硬體的發展來分析的。因此,我也將從這個角度來分析。

階段一

在計算機發展的第一個階段,程式是在CPU中執行,資料在主存中儲存。隨著技術的發展,CPU的速度越來越多高,但是主存的速度卻沒有提高太多。就好比是下面這種情況:

階段二

為瞭解決上面的問題,於是乎出現了快取,裡面存放了一些CPU經常使用的主存資料,快取的速度和CPU差不多,當CPU查詢資料的時候,首先從快取中查詢,沒有的話再從主存中查詢。寫資料的時候,先寫快取的資料,然後再更新到主存中。這樣一種機制使得速度提高很多。

階段三

技術繼續發展,在上面快取的基礎上出現了一級二級三級快取,查詢也是逐層的,第一級快取沒有就到第二層,就這樣以此類推。這時候CPU也得到了快讀發展,由之前的一個核變成了多核CPU(一個CPU變成了多部分)。

這時候呢,之前只能同時跑一個執行緒,現在就能跑很多個執行緒了。而且從上面我們可以看到,每一個核都有相應的快取區,但是主記憶體還是哪一個。既然能同時跑多個執行緒,那速度肯定槓槓滴就上去了吧,不跑不知道,一跑嚇一跳,立馬出現了很多個問題。

問題一:快取一致性問題

也就是說,每一個核都有自己的快取區,但是這些快取區儲存的資料卻不一樣。一張圖就明白了:

問題二:處理器優化和指令重排

這問題的意思是,既然CPU有這麼多核心,肯定是想讓資源得到充分利用,於是把我們寫的程式拆分,對一些程式碼進行亂序處理,這就是處理器優化。而且,java虛擬機器器一看CPU的這個操作真的強,於是就模仿了一下,建立了即時編譯器(JIT),這個編譯器也會做指令重排的操作。很明顯,我們的程式碼順序被打亂,指令被重排,就可能不會按照我們的意願去執行了。

上面出現的這些問題,好像都是從硬體的角度來分析的,《深入理解java虛擬機器器》一書於是引出了軟體的問題,也就是說,上面的這些問題如果轉化到軟體層次會帶來什麼問題呢?

問題三:軟體問題

(1)原子性問題

首先快取一致性問題在程式中會帶來原子性問題,原子性問題是什麼意思呢?你首先就要先理解原子。在生物裡面原子叫做不可再分的物質。在軟體裡面,原子叫做不可再分的程式操作。而原子性問題肯定就是打破了這個規則,也就是說在這個操作中又進行了拆分。

在快取一致性問題中,兩個CPU核心中a的資料不一致,也就是說兩個CPU核心讀取主存a的值是不一樣的。那麼對於a的更改這個操作肯定就不是原子性,在A更改的過程中,兩個CPU核心同時進行了更改。

(2)可見性問題

上面在介紹原子性問題的時候說了,兩個執行緒(CPU核心)訪問同一個變數時,執行緒2修改了這個變數的值,但是執行緒1卻沒有看到其修改,所以讀的仍然是舊資料。

(3)有序性

也就是程式沒有按照指定的順序去執行。

可見性問題和有序性問題就是由於處理器優化和執行重排造成的。

階段四

針對於這麼多問題,於是java虛擬機器器提出了一個java記憶體模型。有效地解決了上面出現的這三個問題:

三、java記憶體模型

1、解釋

為了和開頭進行對照,我們再給出以此他的記憶體模型圖:

從這張圖我們分析一下java記憶體模型是如何解決上面的三個問題的。

規則一:所有的資料都在主記憶體中。

規則二:每個執行緒都保留一份共享變數的副本。執行緒對變數的所有操作都必須在這個副本記憶體中進行,而不能直接讀寫主記憶體。

規則三:不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。

看文字有點亂,我們舉個例子,這個例子我覺得不太恰當,但是結合著上面的概念,相信你會明白的。在古代的時候經常發生旱災,朝廷去賑災,於是拿了一個大鐵鍋煮粥,這個大鐵鍋就是主記憶體,裡面的粥就是資料。而每個難民就代表了不同的執行緒。

(1)難民都有一個碗,盛放一碗粥。就好比是每個執行緒都有一個本地記憶體,有一個主記憶體的副本。

(2)難民喝粥就只能在自己碗裡,不能直接爬到鍋中喝粥,就好比執行緒只能在自己的本地記憶體中操作資料,不能直接到主記憶體讀寫資料。

(3)其中一個難民想要把粥分給夥伴,怎麼辦呢?這個難民要先把粥倒進鍋裡,同伴再去鍋裡盛。就好比執行緒不能直接訪問對方的記憶體,他們之間的資料傳遞都是通過主記憶體。

這個例子希望你能明白。現在這個模型算是出來了,還有一個問題沒有解決,那就是java提供了什麼東西來實現的這三個規則呢?

由於提供的機制太多,我們可以簡單的例舉幾個,比如說synchronized關鍵字保證了原子性,volatile關鍵字保證了可見性。synchronized關鍵字和volatile關鍵字保證了有序性。當然還有很多的Lock機制,併發包裡面等等都是為瞭解決這三個問題提出來的。

2、happens-before原則

其中有一條很重要的規則叫做happens-before原則,這條原則是為瞭解決可見性提出來的。什麼意思呢?

如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。舉個例子:

(1)程式順序規則:一個執行緒中的每個操作發生在後一個操作之前,這就是happens-before。

(2)鎖規則:對於鎖機制,一定要先加鎖,才能解鎖,這也是happens-before。

(3)volatile域規則:對一個volatile域的寫操作一定要發生在讀操作前面。

上面是在程式角度來看的,舉一個最簡單不過的例子,你必須要做飯,才能夠吃到飯。

這篇文章當然不是最詳細的最全面的,只是希望大家有所收穫,如有問題歡迎批評指正