怎么并線(三條電線怎么并線)
831
2022-05-29
在面試中,面試官經(jīng)常喜歡問(wèn):『說(shuō)說(shuō)什么是Java內(nèi)存模型(JMM)?』
面試者內(nèi)心狂喜,這題剛背過(guò):『Java內(nèi)存主要分為五大塊:堆、方法區(qū)、虛擬機(jī)棧、本地方法棧、PC寄存器,balabala……』
面試官會(huì)心一笑,露出一道光芒:『好了,今天的面試先到這里了,回去等通知吧』
一般聽(tīng)到等通知這句話,這場(chǎng)面試大概率就是涼涼了。為什么呢?因?yàn)槊嬖囌吲e(cuò)了概念,面試官是想考察JMM,但是面試者一聽(tīng)到Java內(nèi)存這幾個(gè)關(guān)鍵字就開(kāi)始背誦八股文了。Java內(nèi)存模型(JMM)和 Java 運(yùn)行時(shí)內(nèi)存區(qū)域區(qū)別可大了呢,不要走開(kāi)接著往下看,答應(yīng)我要看完。
為什么要有內(nèi)存模型?
要想回答這個(gè)問(wèn)題,我們需要先弄懂傳統(tǒng)計(jì)算機(jī)硬件內(nèi)存架構(gòu)。好了,我要開(kāi)始畫圖了。
硬件內(nèi)存架構(gòu)
(1)CPU
去過(guò)機(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ì)于寄存器來(lái)說(shuō),通常也可以成為 L2 二級(jí)緩存。相對(duì)于硬盤讀取速度來(lái)說(shuō)內(nèi)存讀取的效率非常高,但是與 CPU 還是相差數(shù)量級(jí),所以在 CPU 和主存間引入了多級(jí)緩存,目的是為了做一下緩沖。
(4)Main Memory
Main Memory 就是主存,主存比 L1、L2 緩存要大很多。
注意:部分高端機(jī)器還有 L3 三級(jí)緩存。
緩存一致性問(wèn)題
由于主存與 CPU 處理器的運(yùn)算能力之間有數(shù)量級(jí)的差距,所以在傳統(tǒng)計(jì)算機(jī)內(nèi)存架構(gòu)中會(huì)引入高速緩存來(lái)作為主存和處理器之間的緩沖,CPU 將常用的數(shù)據(jù)放在高速緩存中,運(yùn)算結(jié)束后 CPU 再講運(yùn)算結(jié)果同步到主存中。
使用高速緩存解決了 CPU 和主存速率不匹配的問(wèn)題,但同時(shí)又引入另外一個(gè)新問(wèn)題:緩存一致性問(wèn)題。
在多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 訪問(wèn)緩存時(shí)遵循一定的協(xié)議,在讀寫數(shù)據(jù)時(shí)根據(jù)協(xié)議進(jìn)行操作,共同來(lái)維護(hù)緩存的一致性。這類協(xié)議有 MSI、MESI、MOSI、和 Dragon Protocol 等。
處理器優(yōu)化和指令重排序
為了提升性能在 CPU 和主內(nèi)存之間增加了高速緩存,但在多線程并發(fā)場(chǎng)景可能會(huì)遇到緩存一致性問(wèn)題。那還有沒(méi)有辦法進(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)代編程語(yǔ)言的編譯器也會(huì)做類似的優(yōu)化,比如像 Java 的即時(shí)編譯器(JIT)會(huì)做指令重排序。
處理器優(yōu)化其實(shí)也是重排序的一種類型,這里總結(jié)一下,重排序可以分為三種類型:
編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義放入前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。
并發(fā)編程的問(wèn)題
上面講了一堆硬件相關(guān)的東西,有些同學(xué)可能會(huì)有點(diǎn)懵,繞了這么大圈,這些東西跟 Java 內(nèi)存模型有啥關(guān)系嗎?不要急咱們慢慢往下看。
熟悉 Java 并發(fā)的同學(xué)肯定對(duì)這三個(gè)問(wèn)題很熟悉:『可見(jiàn)性問(wèn)題』、『原子性問(wèn)題』、『有序性問(wèn)題』。如果從更深層次看這三個(gè)問(wèn)題,其實(shí)就是上面講的『緩存一致性』、『處理器優(yōu)化』、『指令重排序』造成的。
緩存一致性問(wèn)題其實(shí)就是可見(jiàn)性問(wèn)題,處理器優(yōu)化可能會(huì)造成原子性問(wèn)題,指令重排序會(huì)造成有序性問(wèn)題,你看是不是都聯(lián)系上了。
出了問(wèn)題總是要解決的,那有什么辦法呢?首先想到簡(jiǎn)單粗暴的辦法,干掉緩存讓 CPU 直接與主內(nèi)存交互就解決了可見(jiàn)性問(wèn)題,禁止處理器優(yōu)化和指令重排序就解決了原子性和有序性問(wèn)題,但這樣一夜回到解放前了,顯然不可取。
所以技術(shù)前輩們想到了在物理機(jī)器上定義出一套內(nèi)存模型, 規(guī)范內(nèi)存的讀寫操作。內(nèi)存模型解決并發(fā)問(wèn)題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
Java 內(nèi)存模型
同一套內(nèi)存模型規(guī)范,不同語(yǔ)言在實(shí)現(xiàn)上可能會(huì)有些差別。接下來(lái)著重講一下 Java 內(nèi)存模型實(shí)現(xiàn)原理。
Java 運(yùn)行時(shí)內(nèi)存區(qū)域與硬件內(nèi)存的關(guān)系
了解過(guò) JVM 的同學(xué)都知道,JVM 運(yùn)行時(shí)內(nèi)存區(qū)域是分片的,分為棧、堆等,其實(shí)這些都是 JVM 定義的邏輯概念。在傳統(tǒng)的硬件內(nèi)存架構(gòu)中是沒(méi)有棧和堆這種概念。
從圖中可以看出棧和堆既存在于高速緩存中又存在于主內(nèi)存中,所以兩者并沒(méi)有很直接的關(guān)系。
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)存。
不同的線程之間無(wú)法直接訪問(wèn)對(duì)方本地內(nèi)存中的變量。
看文字太枯燥了,我又畫了一張圖:
線程間通信
如果兩個(gè)線程都對(duì)一個(gè)共享變量進(jìn)行操作,共享變量初始值為 1,每個(gè)線程都變量進(jìn)行加 1,預(yù)期共享變量的值為 3。在 JMM 規(guī)范下會(huì)有一系列的操作。
為了更好的控制主內(nèi)存和本地內(nèi)存的交互,Java 內(nèi)存模型定義了八種操作來(lái)實(shí)現(xiàn):
lock:鎖定。作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
unlock:解鎖。作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lá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)存的意思。
有態(tài)度的總結(jié)
由于CPU 和主內(nèi)存間存在數(shù)量級(jí)的速率差,想到了引入了多級(jí)高速緩存的傳統(tǒng)硬件內(nèi)存架構(gòu)來(lái)解決,多級(jí)高速緩存作為 CPU 和主內(nèi)間的緩沖提升了整體性能。解決了速率差的問(wèn)題,卻又帶來(lái)了緩存一致性問(wèn)題。
數(shù)據(jù)同時(shí)存在于高速緩存和主內(nèi)存中,如果不加以規(guī)范勢(shì)必造成災(zāi)難,因此在傳統(tǒng)機(jī)器上又抽象出了內(nèi)存模型。
Java 語(yǔ)言在遵循內(nèi)存模型的基礎(chǔ)上推出了 JMM 規(guī)范,目的是解決由于多線程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題。
為了更精準(zhǔn)控制工作內(nèi)存和主內(nèi)存間的交互,JMM 還定義了八種操作:lock, unlock, read, load,use,assign, store, write。
Spring Security 實(shí)戰(zhàn)干貨:WebSecurity和HttpSecurity的關(guān)系
2021-04-19
爆料:Spring 2021年的一些發(fā)展方向
2021-04-17
Java 任務(wù)調(diào)度
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。