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

也許是東半球最叼的Java內(nèi)存模型

開發(fā) 后端
單核CPU 的多線程也會出現(xiàn)上面的線程不安全的問題,只是產(chǎn)生原因不是多核CPU緩存不一致的問題導(dǎo)致,而是CPU調(diào)度線程切換,多線程局部變量不同步引起的。

[[406082]]

面試官:你好,你先自我介紹一下。

安琪拉:面試官你好,我叫安琪拉,草叢三婊,最強(qiáng)中單,草地摩托車車手,第21套廣播體操推廣者,火球擁有者、不焚者,安琪拉,這是我的簡歷,請過目。

面試官:看你簡歷上寫熟悉多線程編程,跟我講講Java內(nèi)存模型。

安琪拉:講Java內(nèi)存模型前我希望給您講一個故事,從CPU的發(fā)展史說起。

面試官:我喜歡聽故事,你說吧。

安琪拉: 先說現(xiàn)代CPU 架構(gòu)的形成,一切要從馮洛伊曼計算機(jī)體系開始說起!

面試官: 扯的是不是有點(diǎn)遠(yuǎn),你能不能快點(diǎn)進(jìn)入主題!

安琪拉: 你對一個從上海開3個小時車來杭州,真心誠意求職的人就這么沒有耐心的。

面試官: 孽緣,真是孽緣。你講吧。

安琪拉: 下圖就是經(jīng)典的 馮洛伊曼體系結(jié)構(gòu),基本把計算機(jī)的組成模塊都定義好了,現(xiàn)在的計算機(jī)都是以這個體系弄的,其中最核心的就是由運(yùn)算器和控制器組成的中央處理器,就是我們常說的CPU。

馮洛伊曼體系結(jié)構(gòu)

面試官: 這個跟Java內(nèi)存模型有什么關(guān)系?

安琪拉: 不要著急嘛!

安琪拉: 剛才說到馮洛伊曼體系中的CPU,你應(yīng)該聽過摩爾定律吧!就是英特爾創(chuàng)始人戈登·摩爾講的:

集成電路上可容納的晶體管數(shù)目,約每隔18個月便會增加一倍,性能也將提升一倍。

面試官: 聽過的,然后呢?

安琪拉:所以你看到我們電腦CPU 的性能越來越強(qiáng)勁,英特爾CPU 從Intel Core 一直到 Intel Core i7,前些年單核CPU 的晶體管數(shù)量確實(shí)符合摩爾定律,看下面這張圖。

摩爾定律

橫軸為新CPU發(fā)明的年份,縱軸為可容納晶體管的對數(shù)。所有的點(diǎn)近似成一條直線,這意味著晶體管數(shù)目隨年份呈指數(shù)變化,大概每兩年翻一番。

面試官: 后來呢?

安琪拉:別著急啊!后來摩爾定律越來越撐不住了,但是更新?lián)Q代的程序?qū)﹄娔X性能的期望和要求還在不斷上漲,就出現(xiàn)了下面的劇情。

[[406084]]

再不轉(zhuǎn)發(fā)我也要跪了

他為其Pentium 4新一代芯片取消上市而道歉, 近幾年來,英特爾不斷地在增加其處理器的運(yùn)行速度。當(dāng)前最快的一款,其速度已達(dá)3.4GHz,雖然強(qiáng)化處理器的運(yùn)行速度,也增強(qiáng)了芯片運(yùn)作效能,但速度提升卻使得芯片的能源消耗量增加,并衍生出冷卻芯片的問題。

因此,英特爾摒棄將心力集中在提升運(yùn)行速度的做法,在未來幾年,將其芯片轉(zhuǎn)為以多模核心(multi-core)的方式設(shè)計等其他方式,來提升芯片的表現(xiàn)。多模核心的設(shè)計法是將多模核心置入單一芯片中。如此一來,這些核心芯片即能以較緩慢的速度運(yùn)轉(zhuǎn),除了可減少運(yùn)轉(zhuǎn)消耗的能量,也能減少運(yùn)轉(zhuǎn)生成的熱量。此外,集眾核心芯片之力,可提供較單一核心芯片更大的處理能力。—《經(jīng)濟(jì)學(xué)人》

安琪拉:當(dāng)然上面貝瑞特當(dāng)然只是在開玩笑,眼看摩爾定律撐不住了,后來怎么處理的呢?一顆CPU 不行,我們多來幾顆嘛!這就是現(xiàn)在我們常見的多核CPU,四核8G 聽著熟悉不熟悉?當(dāng)然完全依據(jù)馮洛伊曼體系設(shè)計的計算機(jī)也是有缺陷的!

面試官: 什么缺陷?說說看。

安琪拉:CPU 運(yùn)算器的運(yùn)算速度遠(yuǎn)比內(nèi)存讀寫速度快,所以CPU 大部分時間都在等數(shù)據(jù)從內(nèi)存讀取,運(yùn)算完數(shù)據(jù)寫回內(nèi)存。

面試官: 那怎么解決?

安琪拉:因?yàn)镃PU 運(yùn)行速度實(shí)在太快,主存(就是內(nèi)存)的數(shù)據(jù)讀取速度和CPU 運(yùn)算速度差了有幾個數(shù)量級,因此現(xiàn)代計算機(jī)系統(tǒng)通過在CPU 和主存之前加了一層讀寫速度盡可能接近CPU 運(yùn)行速度的高速緩存來做數(shù)據(jù)緩沖,這樣緩存提前從主存獲取數(shù)據(jù),CPU 不再從主存取數(shù)據(jù),而是從緩存取數(shù)據(jù)。這樣就緩解由于主存速度太慢導(dǎo)致的CPU 饑餓的問題。同時CPU 內(nèi)還有寄存器,一些計算的中間結(jié)果臨時放在寄存器內(nèi)。

面試官: 既然你提到緩存,那我問你一個問題,CPU 從緩存讀取數(shù)據(jù)和從內(nèi)存讀取數(shù)據(jù)除了讀取速度的差異?有什么本質(zhì)的區(qū)別嗎?不都是讀數(shù)據(jù)寫數(shù)據(jù),而且加緩存會讓整個體系結(jié)構(gòu)變得更加復(fù)雜。

安琪拉:緩存和主存不僅僅是讀取寫入數(shù)據(jù)速度上的差異,還有另外更大的區(qū)別:研究人員發(fā)現(xiàn)了程序80%的時間在運(yùn)行20% 的代碼,所以緩存本質(zhì)上只要把20%的常用數(shù)據(jù)和指令放進(jìn)來就可以了(是不是和Redis 存放熱點(diǎn)數(shù)據(jù)很像),另外CPU 訪問主存數(shù)據(jù)時存在二個局部性現(xiàn)象:

  • 時間局部性現(xiàn)象

如果一個主存數(shù)據(jù)正在被訪問,那么在近期它被再次訪問的概率非常大。想想你程序大部分時間是不是在運(yùn)行主流程20%的代碼。

  • 空間局部性現(xiàn)象

CPU使用到某塊內(nèi)存區(qū)域數(shù)據(jù),這塊內(nèi)存區(qū)域后面臨近的數(shù)據(jù)很大概率立即會被使用到。這個很好解釋,我們程序經(jīng)常用的數(shù)組、集合(本質(zhì)也是數(shù)組)經(jīng)常會順序訪問(內(nèi)存地址連續(xù)或鄰近)。

因?yàn)檫@二個局部性現(xiàn)象的存在使得緩存的存在可以很大程度上緩解CPU 饑餓的問題。

面試官: 講的是那么回事,那能給我畫一下現(xiàn)在CPU、緩存、主存的關(guān)系圖嗎?

安琪拉:可以。我們來看下現(xiàn)在主流的多核CPU的硬件架構(gòu),如下圖所示。

多核心CPU架構(gòu)

多核心CPU架構(gòu)

安琪拉:現(xiàn)代操作系統(tǒng)一般會有多級緩存(Cache Line),一般有L1、L2,甚至有L3,看下安琪拉的電腦緩存信息,一共4核,三級緩存,L1 緩存(在CPU核心內(nèi))這里沒有顯示出來,這里L(fēng)2 緩存后面括號標(biāo)識了是每個核都有L2 緩存,而L3 緩存沒有標(biāo)識,是因?yàn)長3 緩存是4個核共享的緩存:

安琪拉的電腦緩存

安琪拉的電腦多級緩存

面試官: 那你能跟我簡單講講程序運(yùn)行時,數(shù)據(jù)是怎么在主存、緩存、CPU寄存器之間流轉(zhuǎn)的嗎?

安琪拉:可以。比如以 i = i + 2; 為例, 當(dāng)線程執(zhí)行到這條語句時,會先從主存中讀取i 的值,然后復(fù)制一份到緩存中,CPU 讀取緩存數(shù)據(jù)(取數(shù)指令),進(jìn)行 i + 2 操作(中間數(shù)據(jù)放寄存器),然后把結(jié)果寫入緩存,最后將緩存中i最新的值刷新到主存當(dāng)中(寫回主存時間不確定)。

面試官: 這個數(shù)據(jù)操作邏輯在單線程環(huán)境和多線程環(huán)境下有什么區(qū)別?

安琪拉:比如i 如果是共享變量(例如類的成員變量),單線程運(yùn)行沒有任何問題,但是多線程中運(yùn)行就有可能出問題。

例如:有A、B二個線程,在二個不同的CPU 上運(yùn)行,因?yàn)槊總€線程運(yùn)行的CPU 都有自己的緩存,i是共享變量,初始值是0,A 線程從內(nèi)存讀取i 的值存入緩存,B 線程此時也讀取i 的值存入自己CPU的緩存,A 線程對i 進(jìn)行+1操作,i變成了1,B線程緩存中的變量 i 還是0,B線程也對i 進(jìn)行+1操作,最后A、B線程先后將緩存數(shù)據(jù)寫回內(nèi)存共享區(qū),預(yù)期的結(jié)果應(yīng)該是2,因?yàn)榘l(fā)生了二次+1操作,但是實(shí)際是1。

執(zhí)行過程如下圖:

緩存不一致

緩存不一致

這個就是非常著名的緩存一致性問題,注意這里還只是多CPU的緩存一致性問題,和我們常說的多線程共享變量安全問題還不相同。

說明:單核CPU 的多線程也會出現(xiàn)上面的線程不安全的問題,只是產(chǎn)生原因不是多核CPU緩存不一致的問題導(dǎo)致,而是CPU調(diào)度線程切換,多線程局部變量不同步引起的。

面試官: 那CPU 怎么解決緩存一致性問題呢?

安琪拉:早期的一些CPU 設(shè)計中,是通過鎖總線(總線訪問加Lock# 鎖)的方式解決的??聪翪PU 體系結(jié)構(gòu)圖,如下:

CPU內(nèi)體系結(jié)構(gòu)

CPU內(nèi)體系結(jié)構(gòu)

因?yàn)镃PU 都是通過總線來讀取主存中的數(shù)據(jù),因此對總線加Lock# 鎖的話,其他CPU 訪問主存就被阻塞了,這樣防止了對共享變量的競爭。但是鎖總線對CPU的性能損耗非常大,把多核CPU 并行的優(yōu)勢直接給干沒了!(還記得并發(fā)第一集的并行知識吧)

后面研究人員就搞出了一套協(xié)議:緩存一致性協(xié)議。協(xié)議的類型很多(MSI、MESI、MOSI、Synapse、Firefly),最常見的就是Intel (英特爾)的MESI 協(xié)議。緩存一致性協(xié)議主要規(guī)范了CPU 讀寫主存、管理緩存數(shù)據(jù)的一系列規(guī)范,如下圖所示。

緩存一致性協(xié)議

面試官: 那講講緩存一致性協(xié)議(MESI協(xié)議)唄!

安琪拉: 緩存一致性協(xié)議(MESI協(xié)議)的核心思想:

  • 定義了緩存中的數(shù)據(jù)狀態(tài)只有四種,MESI 是四種狀態(tài)的首字母。
  • 當(dāng)CPU寫數(shù)據(jù)時,如果寫的變量是共享變量,即在其他CPU中也存在該變量的副本,會發(fā)出信號通知其他CPU將該變量的緩存行置為無效狀態(tài);
  • 當(dāng)CPU讀取共享變量時,發(fā)現(xiàn)自己緩存的該變量的緩存行是無效的,那么它就會從內(nèi)存中重新讀取。

緩存中數(shù)據(jù)都是以緩存行(Cache Line)為單位存儲;MESI 各個狀態(tài)描述如下表所示:

面試官: MESI 協(xié)議解決了什么問題?

安琪拉: 解決了**多核CPU **緩存不一致的問題。

面試官: 那我有個疑問了,既然有MESI 的存在,解決多核CPU的緩存一致性,為什么還需要Java用volatile 這種關(guān)鍵字?

因?yàn)槲覀冎纕olatile 也是保證共享變量的可見性。

安琪拉: volatile是Java語言層面來定義的,Java語言實(shí)現(xiàn)volatile 的內(nèi)存可見性需要借助MESI,但是有的CPU只有單核、或者不支持MESI、那怎么實(shí)現(xiàn)內(nèi)存可見呢?可以是通過鎖總線的方式,volatile屏蔽了硬件的差異,說直接點(diǎn):使用volatile 修飾的變量是有內(nèi)存可見性的,這是Java 語法定的,Java 不關(guān)心你底層操作系統(tǒng)、硬件CPU 是如何實(shí)現(xiàn)內(nèi)存可見的,我的語法規(guī)定就是volatile 修飾的變量必須是具有可見性的。

虛擬機(jī)實(shí)現(xiàn)volatile的方式是寫入了一條lock 前綴的匯編指令,lock 前綴的匯編指令會強(qiáng)制變量寫入主存,也可避免前后指令的CPU重排序,并及時讓其他核中的相應(yīng)緩存行失效,volatile是利用MESI達(dá)到符合預(yù)期的效果。

面試官: 你故事講完了嗎?可以說說為什么需要Java內(nèi)存模型了吧?

安琪拉: CPU 有X86(復(fù)雜指令集)、ARM(精簡指令集)等體系架構(gòu),版本類型也有很多種,CPU 可能通過鎖總線、MESI 協(xié)議實(shí)現(xiàn)多核心緩存的一致性。因?yàn)橛杏布牟町愐约熬幾g器和處理器的指令重排優(yōu)化的存在,所以Java 需要一種協(xié)議來規(guī)避硬件平臺的差異,保障同一段代碼在所有平臺運(yùn)行效果一致,這個協(xié)議叫做Java 內(nèi)存模型(Java Memory Model)。

面試官: 詳細(xì)說說。

安琪拉:Java內(nèi)存模型( Java Memory Model),簡稱JMM, 是 Java 中非常重要的一個概念,是Java 并發(fā)編程的核心。JMM 是Java 定義的一套協(xié)議,用來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,讓Java 程序在各種平臺都能有一致的運(yùn)行效果。

面試官:你說Java 定義的一套協(xié)議,那既然是協(xié)議,肯定是約定了一些內(nèi)容,這套協(xié)議規(guī)定了什么內(nèi)容?

安琪拉:是的,協(xié)議這個詞很熟悉,HTTP 協(xié)議、TCP 協(xié)議等。Java內(nèi)存模型(JMM) 協(xié)議定了一套規(guī)范:

所有的變量都存儲在主內(nèi)存中,每個線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程使用到的變量(主內(nèi)存的拷貝),線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。不同線程之間無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來完成,如下圖所示,線程的所有操作都是把主內(nèi)存的數(shù)據(jù)放在自己的工作內(nèi)存進(jìn)行。

面試官:你剛才說了一大堆概念,能詳細(xì)講講嗎?比如你剛才講的所有變量都在主內(nèi)存中,每個線程有自己的工作內(nèi)存,能好好講講什么是主內(nèi)存和工作內(nèi)存嗎?

安琪拉:很多人在這里會有一個誤區(qū),認(rèn)為主內(nèi)存、工作內(nèi)存是物理的內(nèi)存條中的內(nèi)存,實(shí)際上工作內(nèi)存、主內(nèi)存都是Java內(nèi)存模型中的概念模型。

面試官:那我們上一節(jié)說的JVM內(nèi)存區(qū)域劃分,有堆和棧,堆是所有線程共享的,棧是線程私有的,這個和真實(shí)的物理存儲有什么關(guān)系呢?

安琪拉:這個問題非常棒!JMM 中定義的每個線程私有的工作內(nèi)存是抽象的規(guī)范,實(shí)際上工作內(nèi)存和真實(shí)的CPU 內(nèi)存架構(gòu)如下所示,Java 內(nèi)存模型和真實(shí)硬件內(nèi)存架構(gòu)是不同的:

JMM與真實(shí)內(nèi)存架構(gòu)

JMM與真實(shí)內(nèi)存架構(gòu)

JMM 是內(nèi)存模型,是抽象的協(xié)議。首先真實(shí)的內(nèi)存架構(gòu)是沒有區(qū)分堆和棧的,這個Java 的JVM 來做的劃分,另外線程私有的本地內(nèi)存線程??赡馨–PU 寄存器、緩存和主存。堆亦是如此!

面試官: 能具體講講JMM 內(nèi)存模型規(guī)范嗎?

安琪拉: 可以。前面已經(jīng)講了線程本地內(nèi)存和物理真實(shí)內(nèi)存之間的關(guān)系,說的詳細(xì)些:

初始變量首先存儲在主內(nèi)存中;

線程操作變量需要從主內(nèi)存拷貝到線程本地內(nèi)存中;

線程的本地工作內(nèi)存是一個抽象概念,包括了緩存、寄存器、store buffer(CPU內(nèi)的緩存區(qū)域)等。

一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義了以下八種操作(單一操作都是原子的)來完成:

  • lock(鎖定):作用于主內(nèi)存的變量,把一個變量標(biāo)識為一條線程獨(dú)占狀態(tài)。
  • unlock(解鎖):作用于主內(nèi)存變量,把一個處于鎖定狀態(tài)的變量解除鎖定,解除鎖定后的變量才可以被其他線程鎖定。
  • read(讀取):作用于主內(nèi)存變量,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用
  • load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
  • use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
  • assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
  • store(有的指令是save/存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
  • write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時,必須滿足如下規(guī)則:

  • 如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存,需要順序執(zhí)行read 和load 操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store 和write 操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行,也就是操作不是原子的,一組操作可以中斷。
  • 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),必須成對出現(xiàn)。
  • 不允許一個線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
  • 一個新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實(shí)施use和store操作之前,必須先執(zhí)行過了assign和load操作。
  • 一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。lock和unlock必須成對出現(xiàn)
  • 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。

面試官: 并發(fā)編程的三個特征,你知道嗎?

安琪拉: 多線程并發(fā)編程中主要圍繞著三個特性實(shí)現(xiàn)。

  • 可見性

可見性是指當(dāng)多個線程訪問同一個共享變量時,一個線程修改了這個變量的值,其他線程能夠立即看到修改后的值。

  • 原子性

原子性指的一個操作或一組操作要么全部執(zhí)行,要么全部不執(zhí)行。

  • 有序性

有序性是指程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

 

主要JMM的內(nèi)容介紹完了,后面再介紹volatile的時候詳細(xì)說lock指令,并發(fā)編程的原子性、可見性、有序性。

 

責(zé)任編輯:武曉燕 來源: 安琪拉的博客
相關(guān)推薦

2019-08-07 14:52:34

分庫分表數(shù)據(jù)庫

2014-08-07 10:03:31

debug技巧原則

2014-08-07 10:49:20

debugdebug技巧

2021-03-17 08:22:04

JDK16 HotSpot源碼

2023-06-19 08:02:40

2013-12-12 16:25:18

微軟Windows 9Threshold

2020-08-31 11:30:06

編程語言

2015-06-01 10:17:36

2023-03-26 11:26:29

2018-12-21 15:29:20

保險電商職業(yè)

2022-11-16 16:14:46

單踏板模式特斯拉

2019-03-01 15:57:00

服務(wù)器硬件數(shù)據(jù)量

2018-03-27 09:31:21

數(shù)據(jù)庫MySQL線程池

2015-11-10 17:36:18

2009-03-18 17:53:05

SunIBM收購

2018-03-27 23:20:57

互聯(lián)網(wǎng)隱私平臺

2018-07-04 14:43:55

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

2009-06-24 16:50:11

Java內(nèi)存模型

2018-07-16 15:05:43

Redis內(nèi)存數(shù)據(jù)庫

2013-07-11 10:37:20

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

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