Java內存模型是在硬件內存模型上的更高層的抽象,它屏蔽了各類硬件和操做系統訪問的差別性,保證了Java程序在各類平臺下對內存的訪問都能達到一致的效果。java
在正式講解Java的內存模型以前,咱們有必要先了解一下硬件層面的一些東西。git
在現代計算機的硬件體系中,CPU的運算速度是很是快的,遠遠高於它從存儲介質讀取數據的速度,這裏的存儲介質有不少,好比磁盤、光盤、網卡、內存等,這些存儲介質有一個很明顯的特色——距離CPU越近的存儲介質每每越小越貴越快,距離CPU越遠的存儲介質每每越大越便宜越慢。數據庫
因此,在程序運行的過程當中,CPU大部分時間都浪費在了磁盤IO、網絡通信、數據庫訪問上,若是不想讓CPU在那裏白白等待,咱們就必須想辦法去把CPU的運算能力壓榨出來,不然就會形成很大的浪費,而讓CPU同時去處理多項任務則是最容易想到的,也是被證實很是有效的壓榨手段,這也就是咱們常說的「併發執行」。編程
可是,讓CPU併發地執行多項任務並非那麼容易實現的事,由於全部的運算都不可能只依靠CPU的計算就能完成,每每還須要跟內存進行交互,如讀取運算數據、存儲運算結果等。數組
前面咱們也說過了,CPU與內存的交互每每是很慢的,因此這就要求咱們要想辦法在CPU和內存之間創建一種鏈接,使它們達到一種平衡,讓運算能快速地進行,而這種鏈接就是咱們常說的「高速緩存」。緩存
高速緩存的速度是很是接近CPU的,可是它的引入又帶來了新的問題,現代的CPU每每是有多個核心的,每一個核心都有本身的緩存,而多個核心之間是不存在時間片的競爭的,它們能夠並行地執行,那麼,怎麼保證這些緩存與主內存中的數據的一致性就成爲了一個難題。網絡
爲了解決緩存一致性的問題,多個核心在訪問緩存時要遵循一些協議,在讀寫操做時根據協議來操做,這些協議有MSI、MESI、MOSI等,它們定義了什麼時候應該訪問緩存中的數據、什麼時候應該讓緩存失效、什麼時候應該訪問主內存中的數據等基本原則。多線程
而隨着CPU能力的不斷提高,一層緩存就沒法知足要求了,就逐漸衍生出了多級緩存。架構
按照數據讀取順序和CPU的緊密程度,CPU的緩存能夠分爲一級緩存(L1)、二級緩存(L2)、三級緩存(L3),每一級緩存存儲的數據都是下一級的一部分。併發
這三種緩存的技術難度和製做成本是相對遞減的,容量也是相對遞增的。
因此,在有了多級緩存後,程序的運行就變成了:
當CPU要讀取一個數據的時候,先從一級緩存中查找,若是沒找到再從二級緩存中查找,若是沒找到再從三級緩存中查找,若是沒找到再從主內存中查找,而後再把找到的數據依次加載到多級緩存中,下次再使用相關的數據直接從緩存中查找便可。
而加載到緩存中的數據也不是說用到哪一個就加載哪一個,而是加載內存中連續的數據,通常來講是加載連續的64個字節,所以,若是訪問一個 long 類型的數組時,當數組中的一個值被加載到緩存中時,另外 7 個元素也會被加載到緩存中,這就是「緩存行」的概念。
緩存行雖然能極大地提升程序運行的效率,可是在多線程對共享變量的訪問過程當中又帶來了新的問題,也就是很是著名的「僞共享」。
關於僞共享的問題,咱們這裏就不展開講了,有興趣的能夠看彤哥以前發佈的【雜談 什麼是僞共享(false sharing)?】章節的相關內容。
除此以外,爲了使CPU中的運算單元可以充分地被利用,CPU可能會對輸入的代碼進行亂序執行優化,而後在計算以後再將亂序執行的結果進行重組,保證該結果與順序執行的結果一致,但並不保證程序中各個語句計算的前後順序與代碼的輸入順序一致,所以,若是一個計算任務依賴於另外一個計算任務的結果,那麼其順序性並不能靠代碼的前後順序來保證。
與CPU的亂序執行優化相似,java虛擬機的即時編譯器也有相似的指令重排序優化。
爲了解決上面提到的多個緩存讀寫一致性以及亂序排序優化的問題,這就有了內存模型,它定義了共享內存系統中多線程讀寫操做行爲的規範。
Java內存模型(Java Memory Model,JMM)是在硬件內存模型基礎上更高層的抽象,它屏蔽了各類硬件和操做系統對內存訪問的差別性,從而實現讓Java程序在各類平臺下都能達到一致的併發效果。
Java內存模型定義了程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出這樣的底層細節。這裏所說的變量包括實例字段、靜態字段,但不包括局部變量和方法參數,由於它們是線程私有的,它們不會被共享,天然不存在競爭問題。
爲了得到更好的執行效能,Java內存模型並無限制執行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器調整代碼的執行順序等這類權利。
Java內存模型規定了全部的變量都存儲在主內存中,這裏的主內存跟介紹硬件時所用的名字同樣,二者能夠類比,但此處僅指虛擬機中內存的一部分。
除了主內存,每條線程還有本身的工做內存,此處可與CPU的高速緩存進行類比。工做內存中保存着該線程使用到的變量的主內存副本的拷貝,線程對變量的操做都必須在工做內存中進行,包括讀取和賦值等,而不能直接讀寫主內存中的變量,不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞必須經過主內存來完成。
線程、工做內存、主內存三者的關係以下圖所示:
注意,這裏所說的主內存、工做內存跟Java虛擬機內存區域劃分中的堆、棧是不一樣層次的內存劃分,若是二者必定要勉強對應起來,主內存主要對應於堆中對象的實例部分,而工做內存主要對應與虛擬機棧中的部分區域。
從更低層次來講,主內存主要對應於硬件內存部分,工做內存主要對應於CPU的高速緩存和寄存器部分,但也不是絕對的,主內存也可能存在於高速緩存和寄存器中,工做內存也可能存在於硬件內存中。
關於主內存與工做內存之間具體的交互協議,Java內存模型定義瞭如下8種具體的操做來完成:
(1)lock,鎖定,做用於主內存的變量,它把主內存中的變量標識爲一條線程獨佔狀態;
(2)unlock,解鎖,做用於主內存的變量,它把鎖定的變量釋放出來,釋放出來的變量才能夠被其它線程鎖定;
(3)read,讀取,做用於主內存的變量,它把一個變量從主內存傳輸到工做內存中,以便後續的load操做使用;
(4)load,載入,做用於工做內存的變量,它把read操做從主內存獲得的變量放入工做內存的變量副本中;
(5)use,使用,做用於工做內存的變量,它把工做內存中的一個變量傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做;
(6)assign,賦值,做用於工做內存的變量,它把一個從執行引擎接收到的變量賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時使用這個操做;
(7)store,存儲,做用於工做內存的變量,它把工做內存中一個變量的值傳遞到主內存中,以便後續的write操做使用;
(8)write,寫入,做用於主內存的變量,它把store操做從工做內存獲得的變量的值放入到主內存的變量中;
若是要把一個變量從主內存複製到工做內存,那就要按順序地執行read和load操做,一樣地,若是要把一個變量從工做內存同步回主內存,就要按順序地執行store和write操做。注意,這裏只說明瞭要按順序,並無說必定要連續,也就是說能夠在read與load之間、store與write之間插入其它操做。好比,對主內存中的變量a和b的訪問,能夠按照如下順序執行:
read a -> read b -> load b -> load a。
另外,Java內存模型還定義了執行上述8種操做的基本規則:
(1)不容許read和load、store和write操做之一單獨出現,即不容許出現從主內存讀取了而工做內存不接受,或者從工做內存回寫了但主內存不接受的狀況出現;
(2)不容許一個線程丟棄它最近的assign操做,即變量在工做內存變化了必須把該變化同步回主內存;
(3)不容許一個線程無緣由地(即未發生過assign操做)把一個變量從工做內存同步回主內存;
(4)一個新的變量必須在主內存中誕生,不容許工做內存中直接使用一個未被初始化(load或assign)過的變量,換句話說就是對一個變量的use和store操做以前必須執行過load和assign操做;
(5)一個變量同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一個線程執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量才能被解鎖。
(6)若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值;
(7)若是一個變量沒有被lock操做鎖定,則不容許對其執行unlock操做,也不容許unlock一個其它線程鎖定的變量;
(8)對一個變量執行unlock操做以前,必須先把此變量同步回主內存中,即執行store和write操做;
注意,這裏的lock和unlock是實現synchronized的基礎,Java並無把lock和unlock操做直接開放給用戶使用,可是卻提供了兩個更高層次的指令來隱式地使用這兩個操做,即moniterenter和moniterexit。
Java內存模型就是爲了解決多線程環境下共享變量的一致性問題,那麼一致性包含哪些內容呢?
一致性主要包含三大特性:原子性、可見性、有序性,下面咱們就來看看Java內存模型是怎麼實現這三大特性的。
(1)原子性
原子性是指一段操做一旦開始就會一直運行到底,中間不會被其它線程打斷,這段操做能夠是一個操做,也能夠是多個操做。
由Java內存模型來直接保證的原子性操做包括read、load、user、assign、store、write這兩個操做,咱們能夠大體認爲基本類型變量的讀寫是具有原子性的。
若是應用須要一個更大範圍的原子性,Java內存模型還提供了lock和unlock這兩個操做來知足這種需求,儘管不能直接使用這兩個操做,但咱們可使用它們更具體的實現synchronized來實現。
所以,synchronized塊之間的操做也是原子性的。
(2)可見性
可見性是指當一個線程修改了共享變量的值,其它線程能當即感知到這種變化。
Java內存模型是經過在變動修改後同步回主內存,在變量讀取前從主內存刷新變量值來實現的,它是依賴主內存的,不管是普通變量仍是volatile變量都是如此。
普通變量與volatile變量的主要區別是是否會在修改以後當即同步回主內存,以及是否在每次讀取前當即從主內存刷新。所以咱們能夠說volatile變量保證了多線程環境下變量的可見性,但普通變量不能保證這一點。
除了volatile以外,還有兩個關鍵字也能夠保證可見性,它們是synchronized和final。
synchronized的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中,即執行store和write操做」這條規則獲取的。
final的可見性是指被final修飾的字段在構造器中一旦被初始化完成,那麼其它線程中就能看見這個final字段了。
(3)有序性
Java程序中自然的有序性能夠總結爲一句話:若是在本線程中觀察,全部的操做都是有序的;若是在另外一個線程中觀察,全部的操做都是無序的。
前半句是指線程內表現爲串行的語義,後半句是指「指令重排序」現象和「工做內存和主內存同步延遲」現象。
Java中提供了volatile和synchronized兩個關鍵字來保證有序性。
volatile自然就具備有序性,由於其禁止重排序。
synchronized的有序性是由「一個變量同一時刻只容許一條線程對其進行lock操做」這條規則獲取的。
若是Java內存模型的有序性都只依靠volatile和synchronized來完成,那麼有一些操做就會變得很囉嗦,可是咱們在編寫Java併發代碼時並無感覺到,這是由於Java語言自然定義了一個「先行發生」原則,這個原則很是重要,依靠這個原則咱們能夠很容易地判斷在併發環境下兩個操做是否可能存在競爭衝突問題。
先行發生,是指操做A先行發生於操做B,那麼操做A產生的影響可以被操做B感知到,這種影響包括修改了共享內存中變量的值、發送了消息、調用了方法等。
下面咱們看看Java內存模型定義的先行發生原則有哪些:
(1)程序次序原則
在一個線程內,按照程序書寫的順序執行,書寫在前面的操做先行發生於書寫在後面的操做,準確地講是控制流順序而不是代碼順序,由於要考慮分支、循環等狀況。
(2)監視器鎖定原則
一個unlock操做先行發生於後面對同一個鎖的lock操做。
(3)volatile原則
對一個volatile變量的寫操做先行發生於後面對該變量的讀操做。
(4)線程啓動原則
對線程的start()操做先行發生於線程內的任何操做。
(5)線程終止原則
線程中的全部操做先行發生於檢測到線程終止,能夠經過Thread.join()、Thread.isAlive()的返回值檢測線程是否已經終止。
(6)線程中斷原則
對線程的interrupt()的調用先行發生於線程的代碼中檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測是否發生中斷。
(7)對象終結原則
一個對象的初始化完成(構造方法執行結束)先行發生於它的finalize()方法的開始。
(8)傳遞性原則
若是操做A先行發生於操做B,操做B先行發生於操做C,那麼操做A先行發生於操做C。
這裏說的「先行發生」與「時間上的先發生」沒有必然的關係。
好比,下面的代碼:
int a = 0;
// 操做A:線程1對進行賦值操做
a = 1;
// 操做B:線程2獲取a的值
int b = a;
複製代碼
若是線程1在時間順序上先對a進行賦值,而後線程2再獲取a的值,這能說明操做A先行發生於操做B嗎?
顯然不能,由於線程2可能讀取的仍是其工做內存中的值,或者說線程1並無把a的值刷新回主內存呢,這時候線程2讀取到的值可能仍是0。
因此,「時間上的先發生」不必定「先行發生」。
再看一個例子:
// 同一個線程中
int i = 1;
int j = 2;
複製代碼
根據第一條程序次序原則,int i = 1;
先行發生於int j = 2;
,可是因爲處理器優化,可能致使int j = 2;
先執行,可是這並不影響先行發生原則的正確性,由於咱們在這個線程中並不會感知到這點。
因此,「先行發生」不必定「時間上先發生」。
(1)硬件內存架構使得咱們必須創建內存模型來保證多線程環境下對共享內存訪問的正確性;
(2)Java內存模型定義了保證多線程環境下共享變量一致性的規則;
(3)Java內存模型提供了工做內存與主內存交互的8大操做:lock、unlock、read、load、use、assign、store、write;
(4)Java內存模型對原子性、可見性、有序性提供了一些實現;
(5)先行發生的8大原則:程序次序原則、監視器鎖定原則、volatile原則、線程啓動原則、線程終止原則、線程中斷原則、對象終結原則、傳遞性原則;
(6)先行發生不等於時間上的先發生;
Java內存模型是Java中很重要的概念,理解它很是有助於咱們編寫多線程代碼,理解多線程的本質,筆者這裏整理了一些不錯的資料提供給你們。
《深刻理解Java虛擬機》
《Java併發編程的藝術》
《深刻理解java內存模型》
關注個人公衆號「彤哥讀源碼」回覆「JMM」領取上面三本書籍。
歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章,與彤哥一塊兒暢遊源碼的海洋。