Java 內存模型跟上一篇 JVM 內存結構很像,我常常會把他們搞混,但其實它們不是一回事,並且相差還很大的,但願你沒它們搞混,特別是在面試的時候,搞混了的話就會答非所問,影響你的面試成績,固然也許你碰到了半吊子面試官,那就要恭喜你了。Java 內存模型比 JVM 內存結構複雜不少,Java 內存模型有一個規範叫:《JSR 133 :Java內存模型與線程規範》,裏面的內容很豐富,若是你沒看過的話,我建議你看一下。今天咱們就簡單的來聊一聊 Java 內存模型,關於 Java 內存模型,咱們仍是先從硬件內存模型入手。java
先來看看硬件內存簡單架構,以下圖所示:程序員
這是一幅簡單的硬件內存結構圖,真實的結構圖要比這複雜不少,特別是在緩存層,如今的計算機中 CPU 緩存通常有三層,你也能夠打開你的電腦看看,打開 任務資源管理器 ---> 性能 ---> cpu ,以下圖所示:面試
從圖中能夠看出我這臺機器的 CPU 有三級緩存,一級緩存 (L1) 、二級緩存(L2)、三級緩存(L3),一級緩存是最接近 CPU 的,三級緩存是最接近內存的,每一級緩存的數據都是下一級緩存的一部分。三級緩存架構以下圖所示:redis
如今咱們對硬件內存架構有了必定的瞭解,咱們來弄明白一個問題,爲何須要在 CPU 和內存之間添加緩存?編程
關於這個問題咱們就簡單點說,咱們知道 CPU 是高速的,而內存相對來講是低速的,這就會形成一個問題,不能充分的利用 CPU 高速的特色,由於 CPU 每次從內存裏獲取數據的話都須要等待,這樣就浪費了 CPU 高速的性能,緩存的出現就是用來消除 CPU 與內存之間差距的。緩存的速度要大於內存小於 CPU ,加入緩存以後,CPU 直接從緩存中讀取數據,由於緩存仍是比較快的,因此這樣就充分利用了 CPU 高速的特性。但也不是每次都能從緩存中讀取到數據,這個跟咱們項目中使用的 redis 等緩存工具同樣,也存在一個緩存命中率,在 CPU 中,先查找 L1 Cache,若是 L1 Cache 沒有命中,就往 L2 Cache 裏繼續找,依此類推,最後沒找到的話直接從內存中取,而後添加到緩存中。固然當 CPU 須要寫數據到主存時,一樣會先刷新寄存器中的數據到 CPU 緩存,而後再把數據刷新到主內存中。數組
也許你已經看出了這個框架的弊端,在單核時代只有一個處理器核心,讀/寫操做徹底都是由單核完成,沒什麼問題;可是多核架構,一個核修改主存後,其餘核心並不知道數據已經失效,繼續傻傻的使用主存或者本身緩存層的數據,那麼就會致使數據不一致的狀況。關於這個問題 CPU 硬件廠商也提供瞭解決辦法,叫作緩存一致性協議(MESI協議),緩存一致性協議這東西我也不瞭解,我也說不清,因此就不在這裏 BB 了,有興趣的能夠自行研究。緩存
聊完了硬件內存架構,咱們將焦點回到咱們的主題 Java 內存模型上,下面就一塊兒來聊一聊 Java 內存模型。微信
Java 內存模型是什麼?Java 內存模型能夠理解爲遵守多核硬件架構的設計,用 Java 實現了一套 JVM 層面的「緩存一致性」,這樣就能夠規避 CPU 硬件廠商的標準不同帶來的風險。好了,正式介紹一下 Java 內存模型:Java 內存模型 ( Java Memory Model,簡稱 JMM ),自己是種抽象的概念,並非像硬件架構同樣真實存在的,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量 (包括實例字段、靜態字段和構成數組對象的元素) 的訪問方式,更多關於 Java 內存模型知識能夠閱讀 JSR 133 :Java內存模型與線程規範。網絡
咱們知道 JVM 運行程序的實體是線程,在上一篇 JVM 內存結構中咱們得知每一個線程建立時,JVM 都會爲其建立一個工做內存 ( Java 棧 ),用於存儲線程私有數據,而 Java 內存模型中規定全部變量都存儲在主內存,主內存是共享內存區域,全部線程均可以訪問,但線程對變量的操做 ( 讀取賦值等 ) 必須在工做內存中進行,首先要將變量從主內存拷貝到本身的工做內存空間,而後對變量進行操做,操做完後再將變量寫回主內存,不能直接操做主內存中的變量。架構
咱們知道 Java棧是每一個線程私有的數據區域,別的線程沒法訪問到不一樣線程的私有數據,因此線程須要通訊的話,就必須經過主內存來完成,Java 內存模型就是夾在這二者之間的一組規範,咱們先來看看這個抽象架構圖:
從結構圖來看,若是線程 A 與線程 B 之間須要通訊的話,必需要經歷下面 2 個步驟:
咱們來看一個具體的例子來加深一下理解,看下面這張圖:
如今線程 A 須要和線程 B 通訊,咱們已經知道線程之間通訊的兩部曲了,假設初始時,這三個內存中的 x 值都爲 0。線程 A 在執行時,把更新後的 x 值(假設值爲 1)臨時存放在本身的本地內存 A 中。當線程 A 和線程 B 須要通訊時,線程 A 首先會把本身本地內存中修改後的 x 值刷新到主內存中,此時主內存中的 x 值變爲了 1。隨後,線程 B 到主內存中去讀取線程 A 更新後的 x 值,此時線程 B 的本地內存的 x 值也變爲了 1,這樣就完成了一次通訊。
JMM 經過控制主內存與每一個線程的本地內存之間的交互,來爲 Java 程序員提供內存可見性保證。Java 內存模型除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。這套實現也就是咱們經常使用的volatile
、synchronized
、final
等。
Happens-Before 內存模型或許叫作 Happens-Before 原則更爲合適,在 《JSR 133 :Java內存模型與線程規範》中,Happens-Before 內存模型被定義成 Java 內存模型近似模型,Happens-Before 原則要說明的是關於可見性的一組偏序關係。
爲了方便程序員開發,將底層的煩瑣細節屏蔽掉,Java 內存模型 定義了 Happens-Before 原則。只要咱們理解了Happens-Before 原則,無需瞭解 JVM 底層的內存操做,就能夠解決在併發編程中遇到的變量可見性問題。JVM 定義的 Happens-Before 原則是一組偏序關係:對於兩個操做A和B,這兩個操做能夠在不一樣的線程中執行。若是A Happens-Before B,那麼能夠保證,當A操做執行完後,A操做的執行結果對B操做是可見的。
Happens-Before 原則一共包括 8 條,下面咱們一塊兒簡單的學習一下這 8 條規則。
這條規則是指在一個線程中,按照程序順序,前面的操做 Happens-Before 於後續的任意操做。這一條規則仍是很是好理解的,看下面這一段代碼
class Test{ 1 int x ; 2 int y ; 3 public void run(){ 4 y = 20; 5 x = 12; } }
第四行代碼要 Happens-Before 於第五行代碼,也就是按照代碼的順序來。
這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。例以下面的代碼,在進入同步塊以前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫咱們實現的
synchronized (this) { // 此處自動加鎖 // x 是共享變量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此處自動解鎖
對於鎖定規則能夠這樣理解:假設 x 的初始值是 10,線程 A 執行完代碼塊後 x 的值會變成 12(執行完自動釋放鎖),線程 B 進入代碼塊時,可以看到線程 A 對 x 的寫操做,也就是線程 B 可以看到 x==12。
這條規則是指對一個 volatile 變量的寫操做及這個寫操做以前的全部操做 Happens-Before 對這個變量的讀操做及這個讀操做以後的全部操做。
這條規則是指主線程 A 啓動子線程 B 後,子線程 B 可以看到主線程在啓動子線程 B 前的操做。
public class Demo { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println(count); }); count = 12; t1.start(); } }
子線程 t1 可以看見主線程對 count 變量的修改,因此在線程中打印出來的是 12 。這也就是線程啓動規則
這條是關於線程等待的。它是指主線程 A 等待子線程 B 完成(主線程 A 經過調用子線程 B 的 join() 方法實現),當子線程 B 完成後(主線程 A 中 join() 方法返回),主線程可以看到子線程的操做。固然所謂的「看到」,指的是對共享變量的操做。
public class Demo { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // t1 線程修改了變量 count = 12; }); t1.start(); t1.join(); // mian 線程能夠看到 t1 線程改修後的變量 System.out.println(count); } }
一個線程在另外一個線程上調用 interrupt ,Happens-Before 被中斷線程檢測到 interrupt 被調用。
public class Demo { private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { // t1 線程能夠看到被中斷前的數據 System.out.println(count); }); t1.start(); count = 25; // t1 線程被中斷 t1.interrupt(); } }
mian 線程中調用了 t1 線程的 interrupt() 方法,mian 對 count 的修改對 t1 線程是可見的。
一個對象的構造函數執行結束Happens-Before它的finalize()方法的開始。「結束」和「開始」代表在時間上,一個對象的構造函數必須在它的finalize()方法調用時執行完。根據這條原則,能夠確保在對象的finalize方法執行時,該對象的全部field字段值都是可見的。
這條規則是指若是 A Happens-Before B,且 B Happens-Before C,那麼 A Happens- Before C。
目前互聯網上不少大佬都有 Java 內存模型系列教程,若有雷同,請多多包涵了。原創不易,碼字不易,還但願你們多多支持。若文中有所錯誤之處,還望提出,謝謝,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,和平頭哥一塊兒學習,一塊兒進步。