面试官:说说什么是 Java 内存模型(JMM)?
本文禁止轉(zhuǎn)載
- 1. 為什么要有內(nèi)存模型?
- 1.1. 硬件內(nèi)存架構(gòu)
- 1.2. 緩存一致性問題
- 1.3. 處理器優(yōu)化和指令重排序
- 2. 并發(fā)編程的問題
- 3. Java 內(nèi)存模型
- 3.1. Java 運(yùn)行時(shí)內(nèi)存區(qū)域與硬件內(nèi)存的關(guān)系
- 3.2. Java 線程與主內(nèi)存的關(guān)系
- 3.3. 線程間通信
- 4. 有態(tài)度的總結(jié)
在面試中,面試官經(jīng)常喜歡問:『說說什么是Java內(nèi)存模型(JMM)?』
面試者內(nèi)心狂喜,這題剛背過:『Java內(nèi)存主要分為五大塊:堆、方法區(qū)、虛擬機(jī)棧、本地方法棧、PC寄存器,balabala……』
面試官會(huì)心一笑,露出一道光芒:『好了,今天的面試先到這里了,回去等通知吧』
一般聽到等通知這句話,這場(chǎng)面試大概率就是涼涼了。為什么呢?因?yàn)槊嬖囌吲e(cuò)了概念,面試官是想考察JMM,但是面試者一聽到Java內(nèi)存這幾個(gè)關(guān)鍵字就開始背誦八股文了。Java內(nèi)存模型(JMM)和Java運(yùn)行時(shí)內(nèi)存區(qū)域區(qū)別可大了呢,不要走開接著往下看,答應(yīng)我要看完。
1. 為什么要有內(nèi)存模型?
要想回答這個(gè)問題,我們需要先弄懂傳統(tǒng)計(jì)算機(jī)硬件內(nèi)存架構(gòu)。好了,我要開始畫圖了。
1.1. 硬件內(nèi)存架構(gòu)
(1)CPU
去過機(jī)房的同學(xué)都知道,一般在大型服務(wù)器上會(huì)配置多個(gè)CPU,每個(gè)CPU還會(huì)有多個(gè)核,這就意味著多個(gè)CPU或者多個(gè)核可以同時(shí)(并發(fā))工作。如果使用Java 起了一個(gè)多線程的任務(wù),很有可能每個(gè) CPU 都會(huì)跑一個(gè)線程,那么你的任務(wù)在某一刻就是真正并發(fā)執(zhí)行了。
(2)CPU Register
CPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 內(nèi)部集成的,在寄存器上執(zhí)行操作的效率要比在主存上高出幾個(gè)數(shù)量級(jí)。
(3)CPU Cache Memory
CPU Cache Memory也就是 CPU 高速緩存,相對(duì)于寄存器來說,通常也可以成為 L2 二級(jí)緩存。相對(duì)于硬盤讀取速度來說內(nèi)存讀取的效率非常高,但是與 CPU 還是相差數(shù)量級(jí),所以在 CPU 和主存間引入了多級(jí)緩存,目的是為了做一下緩沖。
(4)Main Memory
Main Memory 就是主存,主存比 L1、L2 緩存要大很多。
注意:部分高端機(jī)器還有 L3 三級(jí)緩存。
1.2. 緩存一致性問題
由于主存與 CPU 處理器的運(yùn)算能力之間有數(shù)量級(jí)的差距,所以在傳統(tǒng)計(jì)算機(jī)內(nèi)存架構(gòu)中會(huì)引入高速緩存來作為主存和處理器之間的緩沖,CPU 將常用的數(shù)據(jù)放在高速緩存中,運(yùn)算結(jié)束后 CPU 再講運(yùn)算結(jié)果同步到主存中。
使用高速緩存解決了 CPU 和主存速率不匹配的問題,但同時(shí)又引入另外一個(gè)新問題:緩存一致性問題。
在多CPU的系統(tǒng)中(或者單CPU多核的系統(tǒng)),每個(gè)CPU內(nèi)核都有自己的高速緩存,它們共享同一主內(nèi)存(Main Memory)。當(dāng)多個(gè)CPU的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),CPU 會(huì)將數(shù)據(jù)讀取到緩存中進(jìn)行運(yùn)算,這可能會(huì)導(dǎo)致各自的緩存數(shù)據(jù)不一致。
因此需要每個(gè) CPU 訪問緩存時(shí)遵循一定的協(xié)議,在讀寫數(shù)據(jù)時(shí)根據(jù)協(xié)議進(jìn)行操作,共同來維護(hù)緩存的一致性。這類協(xié)議有 MSI、MESI、MOSI、和 Dragon Protocol 等。
1.3. 處理器優(yōu)化和指令重排序
為了提升性能在 CPU 和主內(nèi)存之間增加了高速緩存,但在多線程并發(fā)場(chǎng)景可能會(huì)遇到緩存一致性問題。那還有沒有辦法進(jìn)一步提升 CPU 的執(zhí)行效率呢?答案是:處理器優(yōu)化。
為了使處理器內(nèi)部的運(yùn)算單元能夠最大化被充分利用,處理器會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行處理,這就是處理器優(yōu)化。
除了處理器會(huì)對(duì)代碼進(jìn)行優(yōu)化處理,很多現(xiàn)代編程語言的編譯器也會(huì)做類似的優(yōu)化,比如像 Java 的即時(shí)編譯器(JIT)會(huì)做指令重排序。
處理器優(yōu)化其實(shí)也是重排序的一種類型,這里總結(jié)一下,重排序可以分為三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
- 指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
2. 并發(fā)編程的問題
上面講了一堆硬件相關(guān)的東西,有些同學(xué)可能會(huì)有點(diǎn)懵,繞了這么大圈,這些東西跟 Java 內(nèi)存模型有啥關(guān)系嗎?不要急咱們慢慢往下看。
熟悉 Java 并發(fā)的同學(xué)肯定對(duì)這三個(gè)問題很熟悉:『可見性問題』、『原子性問題』、『有序性問題』。如果從更深層次看這三個(gè)問題,其實(shí)就是上面講的『緩存一致性』、『處理器優(yōu)化』、『指令重排序』造成的。
緩存一致性問題其實(shí)就是可見性問題,處理器優(yōu)化可能會(huì)造成原子性問題,指令重排序會(huì)造成有序性問題,你看是不是都聯(lián)系上了。
出了問題總是要解決的,那有什么辦法呢?首先想到簡(jiǎn)單粗暴的辦法,干掉緩存讓 CPU 直接與主內(nèi)存交互就解決了可見性問題,禁止處理器優(yōu)化和指令重排序就解決了原子性和有序性問題,但這樣一夜回到解放前了,顯然不可取。
所以技術(shù)前輩們想到了在物理機(jī)器上定義出一套內(nèi)存模型, 規(guī)范內(nèi)存的讀寫操作。內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
3. Java 內(nèi)存模型
同一套內(nèi)存模型規(guī)范,不同語言在實(shí)現(xiàn)上可能會(huì)有些差別。接下來著重講一下 Java 內(nèi)存模型實(shí)現(xiàn)原理。
3.1. Java 運(yùn)行時(shí)內(nèi)存區(qū)域與硬件內(nèi)存的關(guān)系
了解過 JVM 的同學(xué)都知道,JVM 運(yùn)行時(shí)內(nèi)存區(qū)域是分片的,分為棧、堆等,其實(shí)這些都是 JVM 定義的邏輯概念。在傳統(tǒng)的硬件內(nèi)存架構(gòu)中是沒有棧和堆這種概念。
從圖中可以看出棧和堆既存在于高速緩存中又存在于主內(nèi)存中,所以兩者并沒有很直接的關(guān)系。
3.2. Java 線程與主內(nèi)存的關(guān)系
Java 內(nèi)存模型是一種規(guī)范,定義了很多東西:
- 所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。
- 每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的拷貝副本。
- 線程對(duì)變量的所有操作都必須在本地內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。
- 不同的線程之間無法直接訪問對(duì)方本地內(nèi)存中的變量。
看文字太枯燥了,我又畫了一張圖:
3.3. 線程間通信
如果兩個(gè)線程都對(duì)一個(gè)共享變量進(jìn)行操作,共享變量初始值為 1,每個(gè)線程都變量進(jìn)行加 1,預(yù)期共享變量的值為 3。在 JMM 規(guī)范下會(huì)有一系列的操作。
為了更好的控制主內(nèi)存和本地內(nèi)存的交互,Java 內(nèi)存模型定義了八種操作來實(shí)現(xiàn):
- lock:鎖定。作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
- unlock:解鎖。作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read:讀取。作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
- load:載入。作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use:使用。作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign:賦值。作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store:存儲(chǔ)。作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
- write:寫入。作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。
注意:工作內(nèi)存也就是本地內(nèi)存的意思。
4. 有態(tài)度的總結(jié)
由于CPU 和主內(nèi)存間存在數(shù)量級(jí)的速率差,想到了引入了多級(jí)高速緩存的傳統(tǒng)硬件內(nèi)存架構(gòu)來解決,多級(jí)高速緩存作為 CPU 和主內(nèi)間的緩沖提升了整體性能。解決了速率差的問題,卻又帶來了緩存一致性問題。
數(shù)據(jù)同時(shí)存在于高速緩存和主內(nèi)存中,如果不加以規(guī)范勢(shì)必造成災(zāi)難,因此在傳統(tǒng)機(jī)器上又抽象出了內(nèi)存模型。
Java 語言在遵循內(nèi)存模型的基礎(chǔ)上推出了 JMM 規(guī)范,目的是解決由于多線程通過共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來的問題。
為了更精準(zhǔn)控制工作內(nèi)存和主內(nèi)存間的交互,JMM 還定義了八種操作:lock, unlock, read, load,use,assign, store, write。
– End –
關(guān)于Java 內(nèi)存模型還有很多東西沒有展開講,比如說:內(nèi)存屏障、happens-before、鎖機(jī)制、CAS等等。要肝一個(gè)系列了,加油!
作者:雷小帥
推薦一個(gè)Github 開源項(xiàng)目,『Java八股文』Java面試套路,Java進(jìn)階學(xué)習(xí),打破內(nèi)卷拿大廠Offer,升職加薪!https://github.com/CoderLeixiaoshuai/java-eight-part
作者簡(jiǎn)介: ?讀過幾年書:華中科技大學(xué)碩士畢業(yè);
😂浪過幾個(gè)大廠:華為、網(wǎng)易、百度……
😘一直堅(jiān)信技術(shù)能改變世界,愿保持初心,加油技術(shù)人!
微信搜索公眾號(hào)【愛笑的架構(gòu)師】,關(guān)注這個(gè)對(duì)技術(shù)有追求且有趣的打工人。
總結(jié)
以上是生活随笔為你收集整理的面试官:说说什么是 Java 内存模型(JMM)?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教你学会七种维护服务器安全最佳技巧
- 下一篇: (转)用Java获得当前性能信息