JMM定義
Java內(nèi)存模型(Java Memory Model,簡稱JMM)嘱朽,JMM為Java虛擬機(jī)規(guī)范中定義的虛擬模型旭贬,用來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,主要目標(biāo)是定義程序中各個變量的訪問規(guī)則搪泳,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣底層細(xì)節(jié)稀轨。此處的變量與Java編程時所說的變量不一樣,指包括了實(shí)例字段岸军、靜態(tài)字段和構(gòu)成數(shù)組對象的元素奋刽,但是不包括局部變量與方法參數(shù),后者是線程私有的艰赞,不會被共享佣谐,不存在競爭問題。
Java內(nèi)存模型中規(guī)定:
- 線程對變量的所有操作(讀取方妖、賦值)都必須在工作內(nèi)存中進(jìn)行狭魂,而不能直接讀寫主內(nèi)存中的變量
-
不同線程之間無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來完成
線程的工作內(nèi)存中保存了被該線程使用到的變量的主內(nèi)存副本拷貝(不會拷貝整個對象斋泄,只會拷貝線程訪問到的內(nèi)容)
內(nèi)存間交互操作
主內(nèi)存和工作內(nèi)存的交互協(xié)議,Java內(nèi)存模型中定義了8中操作镐牺,虛擬機(jī)必須保證每種操作都是原子的炫掐、不可再分的(即最小單元)(對于double、long類型睬涧,load募胃、store、read和write在某些平臺允許例外)
上面的八種操作具有一定的執(zhí)行規(guī)則:
- 不允許 read 和 load畦浓、 store 和 write 操作之一單獨(dú)出現(xiàn)痹束, 即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受, 或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)讶请。(必須先執(zhí)行read再執(zhí)行l(wèi)oad参袱,先執(zhí)行store在執(zhí)行write,不必保證連續(xù)執(zhí)行)
- 不允許一個線程丟棄它的最近的 assign 操作秽梅, 即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存抹蚀。(assign-->store-->write:每次更新完成立刻寫會主內(nèi)存,保證其他線程可見)
- 不允許一個線程無原因地(沒有發(fā)生過任何 assign 操作)把數(shù)據(jù)從線程的工作內(nèi)存中同步回主內(nèi)存中企垦。
- 一個新的變量只能在主內(nèi)存中 “ 誕生 “环壤,不允許在工作內(nèi)存中直接使用一個未被初始化 (load 或 assign) 的變量,對一個變量實(shí)施use钞诡、store操作之前郑现,必須先執(zhí)行過了load和assign操作。(read-->load-->use:每次使用都重新加載)
- 一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作荧降,但lock操作可以被同一條線程執(zhí)行多次接箫,只有執(zhí)行相同次數(shù)的unlock,變量才會被解鎖朵诫。
- 如果對變量執(zhí)行l(wèi)ock操作辛友,將會清空工作內(nèi)存中該變量的值,在執(zhí)行引擎使用這個變量前剪返,重新執(zhí)行 load或assign操作初始化該變量的值废累。(保證了變量的線程同步)
- 如果沒有l(wèi)ock,就不允許unlock操作脱盲,同時也不允許unlock其他線程鎖定的變量邑滨。
- 對變量執(zhí)行unlock之前,必須把此變量同步回主內(nèi)存中(store钱反、write操作)掖看。
對于long和double類型的特殊規(guī)則
Java對于long和double在模型中定義了比較寬松的規(guī)定:允許虛擬機(jī)將沒有被volatile修飾的64位數(shù)據(jù)的讀寫分為兩次操作進(jìn)行匣距,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store哎壳、read墨礁、write這4個操作的原子性,即long和double的非原子性協(xié)定耳峦。
多線程情況下,可能存在讀到“半個變量”的情況焕毫。但是在實(shí)際的商用虛擬機(jī)中蹲坷,幾乎都選擇把64位的讀寫操作作為原子操作來對待,所以一般情況下不用將long邑飒、double設(shè)置為volatile變量循签。
原子性、可見性疙咸、有序性
JMM圍繞著在并發(fā)過程中如何處理可見性县匠、原子性、有序性這三個特性而建立的模型撒轮。
原子性(Atomicity)
什么是原子性呢乞旦?就是這個操作要么全部做完,要么全部不做题山,不允許做一半兰粉。舉例說明:
a = a + 1;
結(jié)合線程、工作內(nèi)存顶瞳、主內(nèi)存交互關(guān)系圖玖姑,我們知道這段代碼會執(zhí)行以下三個步驟:
- 將a的值從主存拿到線程的工作內(nèi)存中;
- 在工作內(nèi)存中執(zhí)行 +1 操作得到新的結(jié)果慨菱;
- 將工作內(nèi)存中新的結(jié)果寫回主存焰络。
這三個操作分別具有原子性,CPU完全可能在執(zhí)行完其中的某個步驟后去執(zhí)行其他的內(nèi)容(比如線程A執(zhí)行完步驟2符喝,線程B也執(zhí)行了這段代碼且執(zhí)行完成闪彼,A再執(zhí)行步驟3,會覆蓋掉線程B的結(jié)果)协饲,這就可能導(dǎo)致多線程并發(fā)情況下的安全問題备蚓。如果沒有g(shù)et到原子性的重要性,下面看個接地氣的生活“實(shí)例”:
街邊買個煎餅囱稽,掃碼付錢10元郊尝,包含了兩個步驟:你的賬戶扣10元,店家賬戶增加10元战惊,如果整個操作不滿足原子性流昏,你的賬戶扣了錢但店家沒有收到,那店家能放你走么?如果滿足原子性况凉,你的賬戶扣了錢谚鄙,店家賬戶也收到了錢,歡迎下次光臨刁绒。
JMM直接保證的原子性變量操作包括read闷营、load、assign知市、use傻盟、store和write,我們可以大致認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(long和double具有非原子性協(xié)定)嫂丙。但在我們?nèi)粘i_發(fā)中娘赴,我們需要大范圍的原子性保證(例如整個業(yè)務(wù)方法),Java還提供了lock跟啤、unlock诽表,這兩個操作并沒有直接交給用戶使用,利用字節(jié)碼指令monitorenter隅肥、monitorexit來隱式使用這兩個操作竿奏,這兩個字節(jié)碼反映到Java中就是同步塊synchronized關(guān)鍵字,即synchronized可以保證其修飾的代碼塊的原子性腥放。
可見性
可見性是指當(dāng)一個線程修改了共享變量的值议双,其他線程能夠立即得知這個修改。在線程捉片、工作內(nèi)存平痰、主內(nèi)存交互關(guān)系一圖中,我們可以知道Java內(nèi)存模型是通過在工作內(nèi)存修改值后刷新到主存伍纫,讀取時從主存刷新到工作內(nèi)存這種方式來實(shí)現(xiàn)線程間的通信宗雇,無論是volatile關(guān)鍵詞修飾的變量還是普通變量都是采用的這種方式,但volatile變量和普通變量的區(qū)別是:volatile采用內(nèi)存屏障保證了變量的可見性(內(nèi)存屏障(Memory Barrier))莹规,普通變量無法保證可見性赔蒲。synchronized和final也可以保證可見性,synchronized由“對變量執(zhí)行unlock之前良漱,必須把此變量同步回主內(nèi)存中(store舞虱、write操作)”保證;final的可見性指:被final修飾的字段在構(gòu)造器中初始化完成母市,且構(gòu)造器沒有把“this”引用傳遞出去(this引用逃逸矾兜?這個不太懂),那在其他線程中就可以看到final字段的值患久。
有序性
Java程序天然的有序性可以總結(jié)為:如果在本線程內(nèi)觀察椅寺,所有的操作均是有序的(線程內(nèi)表現(xiàn)為串行的語義 As-If-Serial)浑槽;如果在一個線程內(nèi)觀察另一個線程,所有的操作都是無序的(指令重排序返帕、工作內(nèi)存與主存同步延遲)桐玻。
參考文章
- 《深入理解Java虛擬機(jī)》
- 《java并發(fā)編程實(shí)戰(zhàn)》之java內(nèi)存模型
文中部分內(nèi)容摘自《深入理解Java虛擬機(jī)》一書,這本書講得很好荆萤,每次看都有不同的體會镊靴。文章作為個人筆記、總結(jié)链韭,如有錯誤偏竟、不合適的內(nèi)容,留言指正梧油。