<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          小白也能看懂的Java內(nèi)存模型

          共 4271字,需瀏覽 9分鐘

           ·

          2021-03-19 14:01

          前言

          Java并發(fā)編程系列開坑了,Java并發(fā)編程可以說是中高級研發(fā)工程師的必備素養(yǎng),也是中高級崗位面試必問的問題,本系列就是為了帶讀者們系統(tǒng)的一步一步擊破Java并發(fā)編程各個難點,打破屏障,在面試中所向披靡,拿到心儀的offer,Java并發(fā)編程系列文章依然采用圖文并茂的風格,讓小白也能秒懂。

          Java內(nèi)存模型(Java Memory Model)簡稱J M M,作為Java并發(fā)編程系列的開篇,它是Java并發(fā)編程的基礎(chǔ)知識,理解它能讓你更好的明白線程安全到底是怎么一回事

          內(nèi)容大綱

          硬件內(nèi)存模型

          程序是指令與數(shù)據(jù)的集合,計算機執(zhí)行程序時,是C P U在執(zhí)行每條指令,因為C P U要從內(nèi)存讀指令,又要根據(jù)指令指示去內(nèi)存讀寫數(shù)據(jù)做運算,所以執(zhí)行指令就免不了與內(nèi)存打交道,早期內(nèi)存讀寫速度與C P U處理速度差距不大,倒沒什么問題。

          C P U緩存

          隨著C P U技術(shù)快速發(fā)展,C P U的速度越來越快,內(nèi)存卻沒有太大的變化,導(dǎo)致內(nèi)存的讀寫(IO)速度與C P U的處理速度差距越來越大,為了解決這個問題,引入了緩存(Cache)的設(shè)計,在C P U與內(nèi)存之間加上緩存層,這里的緩存層就是指C P U內(nèi)的寄存器與高速緩存L1,L2,L3

          從上圖中可以看出,寄存器最快,主內(nèi)最慢,越快的存儲空間越小,離C P U越近,相反存儲空間越大速度越慢,離C P U越遠。

          C P U如何與內(nèi)存交互

          C P U運行時,會將指令與數(shù)據(jù)從主存復(fù)制到緩存層,后續(xù)的讀寫與運算都是基于緩存層的指令與數(shù)據(jù),運算結(jié)束后,再將結(jié)果從緩存層寫回主存。

          上圖可以看出,C P U基本都是在和緩存層打交道,采用緩存設(shè)計彌補主存與C P U處理速度的差距,這種設(shè)計不僅僅體現(xiàn)在硬件層面,在日常開發(fā)中,那些并發(fā)量高的業(yè)務(wù)場景都能看到,但是凡事都有利弊,緩存雖然加快了速度,同樣也帶來了在多線程場景存在的緩存一致性問題,關(guān)于緩存一致性問題后面會說,這里大家留個印象。

          Java內(nèi)存模型

          Java內(nèi)存模型(Java Memory Model,J M M),后續(xù)都以J M M簡稱,J M M 是建立在硬件內(nèi)存模型基礎(chǔ)上的抽象模型,并不是物理上的內(nèi)存劃分,簡單說,為了使Java虛擬機(Java Virtual Machine,J V M)在各平臺下達到一致的內(nèi)存交互效果,需要屏蔽下游不同硬件模型的交互差異,統(tǒng)一規(guī)范,為上游提供統(tǒng)一的使用接口。

          J M M是保證J V M在各平臺下對計算機內(nèi)存的交互都能保證效果一致的機制及規(guī)范

          抽象結(jié)構(gòu)

          J M M抽象結(jié)構(gòu)劃分為線程本地緩存與主存,每個線程均有自己的本地緩存,本地緩存是線程私有的,主存則是計算機內(nèi)存,它是共享的。

          不難發(fā)現(xiàn)J M M與硬件內(nèi)存模型差別不大,可以簡單的把線程類比成Core核心線程本地緩存類比成緩存層,如下圖所示

          雖然內(nèi)存交互規(guī)范好了,但是多線程場景必然存在線程安全問題(競爭共享資源),為了使多線程能正確的同步執(zhí)行,就需要保證并發(fā)的三大特性可見性、原子性、有序性

          可見性

          當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改,這就是可見性,如果無法保證,就會出現(xiàn)緩存一致性的問題J M M規(guī)定,所有的變量都放在主存中,當線程使用變量時,先從緩存中獲取,緩存未命中,再從主存復(fù)制到緩存,最終導(dǎo)致線程操作的都是自己緩存中的變量。

          線程A執(zhí)行流程

          • 線程A從緩存獲取變量a
          • 緩存未命中,從主存復(fù)制到緩存,此時a0
          • 線程A獲取變量a,執(zhí)行計算
          • 計算結(jié)果1,寫入緩存
          • 計算結(jié)果1,寫入主存

          線程B執(zhí)行流程

          • 線程B從緩存獲取變量a
          • 緩存未命中,從主存復(fù)制到緩存,此時a1
          • 線程B獲取變量a,執(zhí)行計算
          • 計算結(jié)果2,寫入緩存
          • 計算結(jié)果2,寫入主存

          AB兩個線程執(zhí)行完后,線程A與線程B緩存數(shù)據(jù)不一致,這就是緩存一致性問題,一個是1,另一個是2,如果線程A再進行一次+1操作,寫入主存的還是2,也就是說兩個線程對a共進行了3+1,期望的結(jié)果是3,最終得到的結(jié)果卻是2

          解決緩存一致性問題,就要保證可見性,思路也很簡單,變量寫入主存后,把其他線程緩存的該變量清空,這樣其他線程緩存未命中,就會去主存加載。

          線程A執(zhí)行流程

          • 線程A從緩存獲取變量a
          • 緩存未命中,從主存復(fù)制到緩存,此時a0
          • 線程A獲取變量a,執(zhí)行計算
          • 計算結(jié)果1,寫入緩存
          • 計算結(jié)果1,寫入主存,并清空線程B緩存a變量

          線程B執(zhí)行流程

          • 線程B從緩存獲取變量a
          • 緩存未命中,從主存復(fù)制到緩存,此時a1
          • 線程B獲取變量a,執(zhí)行計算
          • 計算結(jié)果2,寫入緩存
          • 計算結(jié)果2,寫入主存,并清空線程A緩存a變量

          AB兩個線程執(zhí)行完后,線程A緩存是空的,此時線程A再進行一次+1操作,會從主存加載(先從緩存中獲取,緩存未命中,再從主存復(fù)制到緩存)得到2,最后寫入主存的是3Java中提供了volatile修飾變量保證可見性(本文重點是J M M,所以不會對volatile做過多的解讀)。

          看似問題都解決了,然而上面描述的場景是建立在理想情況(線程有序的執(zhí)行),實際中線程可能是并發(fā)(交替執(zhí)行),也可能是并行,只保證可見性仍然會有問題,所以還需要保證原子性

          原子性

          原子性是指一個或者多個操作在C P U執(zhí)行的過程中不被中斷的特性,要么執(zhí)行,要不執(zhí)行,不能執(zhí)行到一半,為了直觀的了解什么是原子性,看看下面這段代碼

          int a=0;
          a++;
          • 原子性操作:int a=0只有一步操作,就是賦值
          • 非原子操作:a++有三步操作,讀取值、計算、賦值

          如果多線程場景進行a++操作,僅保證可見性,沒有保證原子性,同樣會出現(xiàn)問題。

          并發(fā)場景(線程交替執(zhí)行)

          • 線程A讀取變量a到緩存,a0
          • 進行+1運算得到結(jié)果1
          • 切換到B線程
          • B線程執(zhí)行完整個流程,a=1寫入主存
          • 線程A恢復(fù)執(zhí)行,把結(jié)果a=1寫入緩存與主存
          • 最終結(jié)果錯誤

          并行場(線程同時執(zhí)行)

          • 線程A與線程B同時執(zhí)行,可能線程A執(zhí)行運算+1的時候,線程B就已經(jīng)全部執(zhí)行完成,也可能兩個線程同時計算完,同時寫入,不管是那種,結(jié)果都是錯誤的。

          為了解決此問題,只要把多個操作變成一步操作,即保證原子性

          Java中提供了synchronized同時滿足有序性、原子性、可見性)可以保證結(jié)果的原子性(注意這里的描述),synchronized保證原子性的原理很簡單,因為synchronized可以對代碼片段上鎖,防止多個線程并發(fā)執(zhí)行同一段代碼(本文重點是J M M,所以不會對synchronized做過多的解讀)。

          并發(fā)場景(線程A與線程B交替執(zhí)行)

          • 線程A獲取鎖成功
          • 線程A讀取變量a到緩存,進行+1運算得到結(jié)果1
          • 此時切換到了B線程
          • 線程B獲取鎖失敗,阻塞等待
          • 切換回線程A
          • 線程A執(zhí)行完所有流程,主存a=1
          • 線程A釋放鎖成功,通知線程B獲取鎖
          • 線程B獲取鎖成功,讀取變量a到緩存,此時a=1
          • 線程B執(zhí)行完所有流程,主存a=2
          • 線程B釋放鎖成功

          并行場景

          • 線程A獲取鎖成功
          • 線程B獲取鎖失敗,阻塞等待
          • 線程A讀取變量a到緩存,進行+1運算得到結(jié)果1
          • 線程A執(zhí)行完所有流程,主存a=1
          • 線程A釋放鎖成功,通知線程B獲取鎖
          • 線程B獲取鎖成功,讀取變量a到緩存,此時a=1
          • 線程B執(zhí)行完所有流程,主存a=2
          • 線程B釋放鎖成功

          synchronized對共享資源代碼段上鎖,達到互斥效果,天然的解決了無法保證原子性、可見性、有序性帶來的問題。

          雖然在并行場A線程還是被中斷了,切換到了B線程,但它依然需要等待A線程執(zhí)行完畢,才能繼續(xù),所以結(jié)果的原子性得到了保證。

          有序性

          在日常搬磚寫代碼時,可能大家都以為,程序運行時就是按照編寫順序執(zhí)行的,但實際上不是這樣,編譯器和處理器為了優(yōu)化性能,會對代碼做重排,所以語句實際執(zhí)行的先后順序與輸入的代碼順序可能一致,這就是指令重排序

          可能讀者們會有疑問“指令重排為什么能優(yōu)化性能?”,其實C P U會對重排后的指令做并行執(zhí)行,達到優(yōu)化性能的效果。

          重排序前的指令

          重排序后的指令

          重排序后,對a操作的指令發(fā)生了改變,節(jié)省了一次Load aStore a,達到性能優(yōu)化效果,這就是重排序帶來的好處。

          重排遵循as-if-serial原則,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果(即不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變),下面這種情況,就屬于數(shù)據(jù)依賴。

          int i = 10
          int j = 10
          //這就是數(shù)據(jù)依賴,int i 與 int j 不能排到 int c下面去
          int c = i + j

          但也僅僅只是針對單線程,多線程場景可沒這種保證,假設(shè)A、B兩個線程,線程A代碼段無數(shù)據(jù)依賴,線程B依賴線程A的結(jié)果,如下圖(假設(shè)保證了可見性

          禁止重排場景(i默認0)

          • 線程A執(zhí)行i = 10
          • 線程A執(zhí)行b = true
          • 線程B執(zhí)行if( b )通過驗證
          • 線程B執(zhí)行i = i + 10
          • 最終結(jié)果i20

          重排場景(i默認0)

          • 線程A執(zhí)行b = true
          • 線程B執(zhí)行if( b )通過驗證
          • 線程B執(zhí)行i = i + 10
          • 線程A執(zhí)行i = 10
          • 最終結(jié)果i10

          為解決重排序,使用Java提供的volatile修飾變量同時保證可見性、有序性,被volatile修飾的變量會加上內(nèi)存屏障禁止排序(本文重點是J M M,所以不會對volatile做過多的解讀)。

          三大特性的保證

          特性volatilesynchronizedLockAtomic
          可見性可以保證可以保證可以保證可以保證
          原子性無法保證可以保證可以保證可以保證
          有序性一定程度保證可以保證可以保證無法保證

          致謝

          非常感謝各位小哥哥小姐姐們能 看到這里,原創(chuàng)不易,文章有幫助可以「點個贊」或「分享與評論」,都是支持(莫要白嫖)!

          愿你我都能奔赴在各自想去的路上,我們下篇文章見!

          最近面試BAT,整理一份面試資料Java面試BAT通關(guān)手冊,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
          獲取方式:關(guān)注公眾號并回復(fù) java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
          明天見(??ω??)??
          瀏覽 48
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色成人视频在线观看 | 草草地址线路①屁屁影院成人 | 亚洲无码在线免费观看视频 | 国产日韩二区 | 成人三级小视频 |