您好,登錄后才能下訂單哦!
這篇文章主要講解了“什么是Java內(nèi)存模型”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“什么是Java內(nèi)存模型”吧!
Java并發(fā)編程系列開(kāi)坑了,Java并發(fā)編程可以說(shuō)是中高級(jí)研發(fā)工程師的必備素養(yǎng),也是中高級(jí)崗位面試必問(wèn)的問(wèn)題,本系列就是為了帶讀者們系統(tǒng)的一步一步擊破Java并發(fā)編程各個(gè)難點(diǎn),打破屏障,在面試中所向披靡,拿到心儀的offer,Java并發(fā)編程系列文章依然采用圖文并茂的風(fēng)格,讓小白也能秒懂。
Java內(nèi)存模型(Java Memory Model
)簡(jiǎn)稱J M M
,作為Java并發(fā)編程系列的開(kāi)篇,它是Java并發(fā)編程的基礎(chǔ)知識(shí),理解它能讓你更好的明白線程安全到底是怎么一回事。
程序是指令與數(shù)據(jù)的集合,計(jì)算機(jī)執(zhí)行程序時(shí),是C P U
在執(zhí)行每條指令,因?yàn)?code>C P U要從內(nèi)存讀指令,又要根據(jù)指令指示去內(nèi)存讀寫(xiě)數(shù)據(jù)做運(yùn)算,所以執(zhí)行指令就免不了與內(nèi)存打交道,早期內(nèi)存讀寫(xiě)速度與C P U
處理速度差距不大,倒沒(méi)什么問(wèn)題。
隨著C P U
技術(shù)快速發(fā)展,C P U
的速度越來(lái)越快,內(nèi)存卻沒(méi)有太大的變化,導(dǎo)致內(nèi)存的讀寫(xiě)(IO
)速度與C P U
的處理速度差距越來(lái)越大,為了解決這個(gè)問(wèn)題,引入了緩存(Cache
)的設(shè)計(jì),在C P U
與內(nèi)存之間加上緩存層,這里的緩存層就是指C P U
內(nèi)的寄存器與高速緩存(L1,L2,L3
)
上圖可以看出,C P U
基本都是在和緩存層打交道,采用緩存設(shè)計(jì)彌補(bǔ)主存與C P U
處理速度的差距,這種設(shè)計(jì)不僅僅體現(xiàn)在硬件層面,在日常開(kāi)發(fā)中,那些并發(fā)量高的業(yè)務(wù)場(chǎng)景都能看到,但是凡事都有利弊,緩存雖然加快了速度,同樣也帶來(lái)了在多線程場(chǎng)景存在的緩存一致性問(wèn)題,關(guān)于緩存一致性問(wèn)題后面會(huì)說(shuō),這里大家留個(gè)印象。
Java內(nèi)存模型(Java Memory Model,J M M
),后續(xù)都以J M M
簡(jiǎn)稱,J M M
是建立在硬件內(nèi)存模型基礎(chǔ)上的抽象模型,并不是物理上的內(nèi)存劃分,簡(jiǎn)單說(shuō),為了使Java
虛擬機(jī)(Java Virtual Machine,J V M
)在各平臺(tái)下達(dá)到一致的內(nèi)存交互效果,需要屏蔽下游不同硬件模型的交互差異,統(tǒng)一規(guī)范,為上游提供統(tǒng)一的使用接口。
J M M
是保證J V M
在各平臺(tái)下對(duì)計(jì)算機(jī)內(nèi)存的交互都能保證效果一致的機(jī)制及規(guī)范。
不難發(fā)現(xiàn)J M M
與硬件內(nèi)存模型差別不大,可以簡(jiǎn)單的把線程類比成Core核心,線程本地緩存類比成緩存層,如下圖所示
線程A執(zhí)行流程
線程A
從緩存獲取變量a
緩存未命中,從主存復(fù)制到緩存,此時(shí)a
是0
線程A
獲取變量a
,執(zhí)行計(jì)算
計(jì)算結(jié)果1
,寫(xiě)入緩存
計(jì)算結(jié)果1
,寫(xiě)入主存
線程B執(zhí)行流程
線程B
從緩存獲取變量a
緩存未命中,從主存復(fù)制到緩存,此時(shí)a
是1
線程B
獲取變量a,執(zhí)行計(jì)算
計(jì)算結(jié)果2
,寫(xiě)入緩存
計(jì)算結(jié)果2
,寫(xiě)入主存
A
、B
兩個(gè)線程執(zhí)行完后,線程A
與線程B
緩存數(shù)據(jù)不一致,這就是緩存一致性問(wèn)題,一個(gè)是1
,另一個(gè)是2
,如果線程A
再進(jìn)行一次+1
操作,寫(xiě)入主存的還是2
,也就是說(shuō)兩個(gè)線程對(duì)a
共進(jìn)行了3
次+1
,期望的結(jié)果是3
,最終得到的結(jié)果卻是2
。
解決緩存一致性問(wèn)題,就要保證可見(jiàn)性,思路也很簡(jiǎn)單,變量寫(xiě)入主存后,把其他線程緩存的該變量清空,這樣其他線程緩存未命中,就會(huì)去主存加載。
線程A執(zhí)行流程
線程A
從緩存獲取變量a
緩存未命中,從主存復(fù)制到緩存,此時(shí)a
是0
線程A
獲取變量a
,執(zhí)行計(jì)算
計(jì)算結(jié)果1
,寫(xiě)入緩存
計(jì)算結(jié)果1
,寫(xiě)入主存,并清空線程B
緩存a
變量
線程B執(zhí)行流程
線程B
從緩存獲取變量a
緩存未命中,從主存復(fù)制到緩存,此時(shí)a
是1
線程B
獲取變量a,執(zhí)行計(jì)算
計(jì)算結(jié)果2
,寫(xiě)入緩存
計(jì)算結(jié)果2
,寫(xiě)入主存,并清空線程A
緩存a
變量
A
、B
兩個(gè)線程執(zhí)行完后,線程A
緩存是空的,此時(shí)線程A再進(jìn)行一次+1
操作,會(huì)從主存加載(先從緩存中獲取,緩存未命中,再?gòu)闹鞔鎻?fù)制到緩存)得到2
,最后寫(xiě)入主存的是3
,Java
中提供了volatile
修飾變量保證可見(jiàn)性(本文重點(diǎn)是J M M
,所以不會(huì)對(duì)volatile
做過(guò)多的解讀)。
看似問(wèn)題都解決了,然而上面描述的場(chǎng)景是建立在理想情況(線程有序的執(zhí)行),實(shí)際中線程可能是并發(fā)(交替執(zhí)行),也可能是并行,只保證可見(jiàn)性仍然會(huì)有問(wèn)題,所以還需要保證原子性。
原子性是指一個(gè)或者多個(gè)操作在C P U
執(zhí)行的過(guò)程中不被中斷的特性,要么執(zhí)行,要不執(zhí)行,不能執(zhí)行到一半,為了直觀的了解什么是原子性,看看下面這段代碼
int a=0; a++;
原子性操作:int a=0
只有一步操作,就是賦值
非原子操作:a++
有三步操作,讀取值、計(jì)算、賦值
如果多線程場(chǎng)景進(jìn)行a++
操作,僅保證可見(jiàn)性,沒(méi)有保證原子性,同樣會(huì)出現(xiàn)問(wèn)題。
并發(fā)場(chǎng)景(線程交替執(zhí)行)
線程A
讀取變量a
到緩存,a
是0
進(jìn)行+1
運(yùn)算得到結(jié)果1
切換到B
線程
B
線程執(zhí)行完整個(gè)流程,a=1
寫(xiě)入主存
線程A
恢復(fù)執(zhí)行,把結(jié)果a=1
寫(xiě)入緩存與主存
最終結(jié)果錯(cuò)誤
并行場(chǎng)(線程同時(shí)執(zhí)行)
線程A
與線程B
同時(shí)執(zhí)行,可能線程A
執(zhí)行運(yùn)算+1
的時(shí)候,線程B
就已經(jīng)全部執(zhí)行完成,也可能兩個(gè)線程同時(shí)計(jì)算完,同時(shí)寫(xiě)入,不管是那種,結(jié)果都是錯(cuò)誤的。
為了解決此問(wèn)題,只要把多個(gè)操作變成一步操作,即保證原子性。
并發(fā)場(chǎng)景(線程A
與線程B
交替執(zhí)行)
線程A
獲取鎖成功
線程A
讀取變量a
到緩存,進(jìn)行+1
運(yùn)算得到結(jié)果1
此時(shí)切換到了B
線程
線程B
獲取鎖失敗,阻塞等待
切換回線程A
線程A
執(zhí)行完所有流程,主存a=1
線程A釋放鎖成功,通知線程B
獲取鎖
線程B獲取鎖成功,讀取變量a
到緩存,此時(shí)a=1
線程B執(zhí)行完所有流程,主存a=2
線程B釋放鎖成功
并行場(chǎng)景
線程A
獲取鎖成功
線程B
獲取鎖失敗,阻塞等待
線程A
讀取變量a
到緩存,進(jìn)行+1
運(yùn)算得到結(jié)果1
線程A
執(zhí)行完所有流程,主存a=1
線程A
釋放鎖成功,通知線程B
獲取鎖
線程B
獲取鎖成功,讀取變量a
到緩存,此時(shí)a=1
線程B
執(zhí)行完所有流程,主存a=2
線程B
釋放鎖成功
synchronized
對(duì)共享資源代碼段上鎖,達(dá)到互斥效果,天然的解決了無(wú)法保證原子性、可見(jiàn)性、有序性帶來(lái)的問(wèn)題。
雖然在并行場(chǎng)A
線程還是被中斷了,切換到了B
線程,但它依然需要等待A
線程執(zhí)行完畢,才能繼續(xù),所以結(jié)果的原子性得到了保證。
在日常搬磚寫(xiě)代碼時(shí),可能大家都以為,程序運(yùn)行時(shí)就是按照編寫(xiě)順序執(zhí)行的,但實(shí)際上不是這樣,編譯器和處理器為了優(yōu)化性能,會(huì)對(duì)代碼做重排,所以語(yǔ)句實(shí)際執(zhí)行的先后順序與輸入的代碼順序可能一致,這就是指令重排序。
可能讀者們會(huì)有疑問(wèn)“指令重排為什么能優(yōu)化性能?”,其實(shí)C P U
會(huì)對(duì)重排后的指令做并行執(zhí)行,達(dá)到優(yōu)化性能的效果。
重排序前的指令
重排序后,對(duì)a
操作的指令發(fā)生了改變,節(jié)省了一次Load a
和Store a
,達(dá)到性能優(yōu)化效果,這就是重排序帶來(lái)的好處。
重排遵循as-if-serial
原則,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果(即不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變),下面這種情況,就屬于數(shù)據(jù)依賴。
int i = 10int j = 10//這就是數(shù)據(jù)依賴,int i 與 int j 不能排到 int c下面去int c = i + j
但也僅僅只是針對(duì)單線程,多線程場(chǎng)景可沒(méi)這種保證,假設(shè)A、B
兩個(gè)線程,線程A
代碼段無(wú)數(shù)據(jù)依賴,線程B
依賴線程A
的結(jié)果,如下圖(假設(shè)保證了可見(jiàn)性)
禁止重排場(chǎng)景(i默認(rèn)0)
線程A
執(zhí)行i = 10
線程A
執(zhí)行b = true
線程B
執(zhí)行if( b )
通過(guò)驗(yàn)證
線程B
執(zhí)行i = i + 10
最終結(jié)果i
是20
重排場(chǎng)景(i默認(rèn)0)
線程A
執(zhí)行b = true
線程B
執(zhí)行if( b )
通過(guò)驗(yàn)證
線程B
執(zhí)行i = i + 10
線程A
執(zhí)行i = 10
最終結(jié)果i
是10
為解決重排序,使用Java提供的volatile
修飾變量同時(shí)保證可見(jiàn)性、有序性,被volatile
修飾的變量會(huì)加上內(nèi)存屏障禁止排序(本文重點(diǎn)是J M M
,所以不會(huì)對(duì)volatile
做過(guò)多的解讀)。
特性 | volatile | synchronized | Lock | Atomic |
---|---|---|---|---|
可見(jiàn)性 | 可以保證 | 可以保證 | 可以保證 | 可以保證 |
原子性 | 無(wú)法保證 | 可以保證 | 可以保證 | 可以保證 |
有序性 | 一定程度保證 | 可以保證 | 可以保證 | 無(wú)法保證 |
感謝各位的閱讀,以上就是“什么是Java內(nèi)存模型”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)什么是Java內(nèi)存模型這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。