自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

硬件內(nèi)存模型到 Java 內(nèi)存模型,這些硬核知識你知多少?

開發(fā) 后端
Java 內(nèi)存模型跟上一篇 JVM 內(nèi)存結(jié)構(gòu)很像,我經(jīng)常會把他們搞混,但其實它們不是一回事,而且相差還很大的,希望你沒它們搞混,特別是在面試的時候,搞混了的話就會答非所問,影響你的面試成績,當(dāng)然也許你碰到了半吊子面試官,那就要恭喜你了。

 Java 內(nèi)存模型跟上一篇 JVM 內(nèi)存結(jié)構(gòu)很像,我經(jīng)常會把他們搞混,但其實它們不是一回事,而且相差還很大的,希望你沒它們搞混,特別是在面試的時候,搞混了的話就會答非所問,影響你的面試成績,當(dāng)然也許你碰到了半吊子面試官,那就要恭喜你了。Java 內(nèi)存模型比 JVM 內(nèi)存結(jié)構(gòu)復(fù)雜很多,Java 內(nèi)存模型有一個規(guī)范叫:《JSR 133 :Java 內(nèi)存模型與線程規(guī)范》,里面的內(nèi)容很豐富,如果你沒看過的話,我建議你看一下。今天我們就簡單的來聊一聊 Java 內(nèi)存模型,關(guān)于 Java 內(nèi)存模型,我們還是先從硬件內(nèi)存模型入手。 

[[281757]]

硬件內(nèi)存模型

先來看看硬件內(nèi)存簡單架構(gòu),如下圖所示: 

硬件內(nèi)存結(jié)構(gòu) 

這是一幅簡單的硬件內(nèi)存結(jié)構(gòu)圖,真實的結(jié)構(gòu)圖要比這復(fù)雜很多,特別是在緩存層,現(xiàn)在的計算機中 CPU 緩存一般有三層,你也可以打開你的電腦看看,打開 任務(wù)資源管理器 ---> 性能 ---> cpu ,如下圖所示:

  

CPU 緩存

從圖中可以看出我這臺機器的 CPU 有三級緩存,一級緩存 (L1) 、二級緩存(L2)、三級緩存(L3),一級緩存是最接近 CPU 的,三級緩存是最接近內(nèi)存的,每一級緩存的數(shù)據(jù)都是下一級緩存的一部分。三級緩存架構(gòu)如下圖所示:

  

圖片來源網(wǎng)絡(luò)

現(xiàn)在我們對硬件內(nèi)存架構(gòu)有了一定的了解,我們來弄明白一個問題,為什么需要在 CPU 和內(nèi)存之間添加緩存?

關(guān)于這個問題我們就簡單點說,我們知道 CPU 是高速的,而內(nèi)存相對來說是低速的,這就會造成一個問題,不能充分的利用 CPU 高速的特點,因為 CPU 每次從內(nèi)存里獲取數(shù)據(jù)的話都需要等待,這樣就浪費了 CPU 高速的性能,緩存的出現(xiàn)就是用來消除 CPU 與內(nèi)存之間差距的。緩存的速度要大于內(nèi)存小于 CPU ,加入緩存之后,CPU 直接從緩存中讀取數(shù)據(jù),因為緩存還是比較快的,所以這樣就充分利用了 CPU 高速的特性。但也不是每次都能從緩存中讀取到數(shù)據(jù),這個跟我們項目中使用的 redis 等緩存工具一樣,也存在一個緩存命中率,在 CPU 中,先查找 L1 Cache,如果 L1 Cache 沒有命中,就往 L2 Cache 里繼續(xù)找,依此類推,最后沒找到的話直接從內(nèi)存中取,然后添加到緩存中。當(dāng)然當(dāng) CPU 需要寫數(shù)據(jù)到主存時,同樣會先刷新寄存器中的數(shù)據(jù)到 CPU 緩存,然后再把數(shù)據(jù)刷新到主內(nèi)存中。

也許你已經(jīng)看出了這個框架的弊端,在單核時代只有一個處理器核心,讀/寫操作完全都是由單核完成,沒什么問題;但是多核架構(gòu),一個核修改主存后,其他核心并不知道數(shù)據(jù)已經(jīng)失效,繼續(xù)傻傻的使用主存或者自己緩存層的數(shù)據(jù),那么就會導(dǎo)致數(shù)據(jù)不一致的情況。關(guān)于這個問題 CPU 硬件廠商也提供了解決辦法,叫做緩存一致性協(xié)議(MESI 協(xié)議),緩存一致性協(xié)議這東西我也不了解,我也說不清,所以就不在這里 BB 了,有興趣的可以自行研究。

聊完了硬件內(nèi)存架構(gòu),我們將焦點回到我們的主題 Java 內(nèi)存模型上,下面就一起來聊一聊 Java 內(nèi)存模型。

Java 內(nèi)存模型

Java 內(nèi)存模型是什么?Java 內(nèi)存模型可以理解為遵照多核硬件架構(gòu)的設(shè)計,用 Java 實現(xiàn)了一套 JVM 層面的“緩存一致性”,這樣就可以規(guī)避 CPU 硬件廠商的標(biāo)準(zhǔn)不一樣帶來的風(fēng)險。好了,正式介紹一下 Java 內(nèi)存模型:Java 內(nèi)存模型 ( Java Memory Model,簡稱 JMM ),本身是種抽象的概念,并不是像硬件架構(gòu)一樣真實存在的,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量 (包括實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素) 的訪問方式,更多關(guān)于 Java 內(nèi)存模型知識可以閱讀 JSR 133 :Java 內(nèi)存模型與線程規(guī)范。

我們知道 JVM 運行程序的實體是線程,在上一篇 JVM 內(nèi)存結(jié)構(gòu)中我們得知每個線程創(chuàng)建時,JVM 都會為其創(chuàng)建一個工作內(nèi)存 ( Java 棧 ),用于存儲線程私有數(shù)據(jù),而 Java 內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作 ( 讀取賦值等 ) 必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量。

我們知道 Java 棧是每個線程私有的數(shù)據(jù)區(qū)域,別的線程無法訪問到不同線程的私有數(shù)據(jù),所以線程需要通信的話,就必須通過主內(nèi)存來完成,Java 內(nèi)存模型就是夾在這兩者之間的一組規(guī)范,我們先來看看這個抽象架構(gòu)圖:

圖片來源網(wǎng)絡(luò)

從結(jié)構(gòu)圖來看,如果線程 A 與線程 B 之間需要通信的話,必須要經(jīng)歷下面 2 個步驟:

  1. 首先,線程 A 把本地內(nèi)存 A 中的共享變量副本中的值刷新到主內(nèi)存中去。
  2. 然后,線程 B 到主內(nèi)存中去讀取線程 A 更新之后的值,這樣線程 A 中的變量值就到了線程 B 中。

我們來看一個具體的例子來加深一下理解,看下面這張圖:

圖片來源網(wǎng)絡(luò) 

現(xiàn)在線程 A 需要和線程 B 通信,我們已經(jīng)知道線程之間通信的兩部曲了,假設(shè)初始時,這三個內(nèi)存中的 x 值都為 0。線程 A 在執(zhí)行時,把更新后的 x 值(假設(shè)值為 1)臨時存放在自己的本地內(nèi)存 A 中。當(dāng)線程 A 和線程 B 需要通信時,線程 A 首先會把自己本地內(nèi)存中修改后的 x 值刷新到主內(nèi)存中,此時主內(nèi)存中的 x 值變?yōu)榱?1。隨后,線程 B 到主內(nèi)存中去讀取線程 A 更新后的 x 值,此時線程 B 的本地內(nèi)存的 x 值也變?yōu)榱? 1,這樣就完成了一次通信。

JMM 通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為 Java 程序員提供內(nèi)存可見性保證。Java 內(nèi)存模型除了定義了一套規(guī)范,還提供了一系列原語,封裝了底層實現(xiàn)后,供開發(fā)者直接使用。這套實現(xiàn)也就是我們常用的volatile、synchronized、final 等。

Happens-Before內(nèi)存模型

Happens-Before 內(nèi)存模型或許叫做 Happens-Before 原則更為合適,在 《JSR 133 :Java 內(nèi)存模型與線程規(guī)范》中,Happens-Before 內(nèi)存模型被定義成 Java 內(nèi)存模型近似模型,Happens-Before 原則要說明的是關(guān)于可見性的一組偏序關(guān)系。

為了方便程序員開發(fā),將底層的繁瑣細(xì)節(jié)屏蔽掉,Java 內(nèi)存模型 定義了 Happens-Before 原則。只要我們理解了 Happens-Before 原則,無需了解 JVM 底層的內(nèi)存操作,就可以解決在并發(fā)編程中遇到的變量可見性問題。JVM 定義的 Happens-Before 原則是一組偏序關(guān)系:對于兩個操作 A 和 B,這兩個操作可以在不同的線程中執(zhí)行。如果 A Happens-Before B,那么可以保證,當(dāng) A 操作執(zhí)行完后,A 操作的執(zhí)行結(jié)果對 B 操作是可見的。

Happens-Before 原則一共包括 8 條,下面我們一起簡單的學(xué)習(xí)一下這 8 條規(guī)則。

1、程序順序規(guī)則

這條規(guī)則是指在一個線程中,按照程序順序,前面的操作 Happens-Before 于后續(xù)的任意操作。這一條規(guī)則還是非常好理解的,看下面這一段代碼

  1. class Test{ 
  2. 1   int x ; 
  3. 2   int y ; 
  4. 3   public void run(){ 
  5. 4       y = 20; 
  6. 5       x = 12; 
  7.     } 

第四行代碼要 Happens-Before 于第五行代碼,也就是按照代碼的順序來。

2、鎖定規(guī)則

這條規(guī)則是指對一個鎖的解鎖 Happens-Before 于后續(xù)對這個鎖的加鎖。例如下面的代碼,在進(jìn)入同步塊之前,會自動加鎖,而在代碼塊執(zhí)行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫我們實現(xiàn)的

  1. synchronized (this) { 
  2.     // 此處自動加鎖 
  3.     // x 是共享變量, 初始值 =10 
  4.     if (this.x < 12) { 
  5.        this.x = 12; 
  6.     } 
  7. } // 此處自動解鎖 

對于鎖定規(guī)則可以這樣理解:假設(shè) x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的值會變成 12(執(zhí)行完自動釋放鎖),線程 B 進(jìn)入代碼塊時,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能夠看到 x==12。

3、volatile 變量規(guī)則

這條規(guī)則是指對一個 volatile 變量的寫操作及這個寫操作之前的所有操作 Happens-Before 對這個變量的讀操作及這個讀操作之后的所有操作。

4、線程啟動規(guī)則

這條規(guī)則是指主線程 A 啟動子線程 B 后,子線程 B 能夠看到主線程在啟動子線程 B 前的操作。

  1. public class Demo { 
  2.     private static int count = 0; 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         Thread t1 = new Thread(() -> { 
  5.             System.out.println(count); 
  6.         }); 
  7.         count = 12; 
  8.         t1.start(); 
  9.     } 

子線程 t1 能夠看見主線程對 count 變量的修改,所以在線程中打印出來的是 12 。這也就是線程啟動規(guī)則

5、線程結(jié)束規(guī)則

這條是關(guān)于線程等待的。它是指主線程 A 等待子線程 B 完成(主線程 A 通過調(diào)用子線程 B 的 join() 方法實現(xiàn)),當(dāng)子線程 B 完成后(主線程 A 中 join() 方法返回),主線程能夠看到子線程的操作。當(dāng)然所謂的“看到”,指的是對共享變量的操作。

  1. public class Demo { 
  2.     private static int count = 0; 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         Thread t1 = new Thread(() -> { 
  5.             // t1 線程修改了變量 
  6.             count = 12; 
  7.         }); 
  8.         t1.start(); 
  9.         t1.join(); 
  10.         // mian 線程可以看到 t1 線程改修后的變量 
  11.         System.out.println(count); 
  12.     } 

6、中斷規(guī)則

一個線程在另一個線程上調(diào)用 interrupt ,Happens-Before 被中斷線程檢測到 interrupt 被調(diào)用。

  1. public class Demo { 
  2.     private static int count = 0; 
  3.     public static void main(String[] args) throws InterruptedException { 
  4.         Thread t1 = new Thread(() -> { 
  5.             // t1 線程可以看到被中斷前的數(shù)據(jù) 
  6.             System.out.println(count); 
  7.         }); 
  8.         t1.start(); 
  9.         count = 25; 
  10.         // t1 線程被中斷 
  11.         t1.interrupt(); 
  12.     } 

mian 線程中調(diào)用了 t1 線程的 interrupt() 方法,mian 對 count 的修改對 t1 線程是可見的。

7、終結(jié)器規(guī)則

一個對象的構(gòu)造函數(shù)執(zhí)行結(jié)束 Happens-Before 它的 finalize()方法的開始。“結(jié)束”和“開始”表明在時間上,一個對象的構(gòu)造函數(shù)必須在它的 finalize()方法調(diào)用時執(zhí)行完。根據(jù)這條原則,可以確保在對象的 finalize 方法執(zhí)行時,該對象的所有 field 字段值都是可見的。

8、傳遞性規(guī)則

這條規(guī)則是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens- Before C。

責(zé)任編輯:華軒 來源: 平頭哥的技術(shù)博文
相關(guān)推薦

2024-07-26 10:23:52

2025-02-20 14:52:02

2018-07-04 14:43:55

對象模型內(nèi)存結(jié)構(gòu)內(nèi)存模型

2009-06-24 16:50:11

Java內(nèi)存模型

2018-11-20 09:37:19

Java內(nèi)存模型

2011-08-05 15:32:44

2024-06-18 14:01:17

2023-07-11 08:43:43

volatileJava內(nèi)存

2025-03-04 10:45:19

JVM內(nèi)存模型Java

2020-06-28 11:44:02

IO模型計算機

2013-07-11 10:37:20

Java內(nèi)存模型

2018-12-18 14:08:01

Java內(nèi)存volatile

2020-12-07 06:23:48

Java內(nèi)存

2022-07-07 08:00:51

Java內(nèi)存模型

2010-09-25 12:38:40

JVM內(nèi)存模型

2022-06-30 08:52:33

GoC++內(nèi)存模型

2019-06-18 07:55:30

WindowsWindows 10操作系統(tǒng)

2018-01-16 22:36:06

數(shù)據(jù)中心機房空調(diào)系統(tǒng)

2024-02-22 15:36:23

Java內(nèi)存模型線程

2021-05-17 08:18:35

Java內(nèi)存模型JMM
點贊
收藏

51CTO技術(shù)棧公眾號