關(guān)于JMM的思考
前言
看《Java并發(fā)編程的藝術(shù)》總在思考一個問題,JMM到底是個什么東西?我們又需要JMM來討論什么問題狠持?JMM中規(guī)定的happens-before規(guī)則到底決定了什么摊滔,有什么意義?
然而思考了很久槐脏,礙于水平有限并不能完全清楚的解答這一系列的問題喉童,但還是決定將最近的一點思考記錄下來。萬一以后想明白了呢顿天。
那么一個疑問就是關(guān)于JMM本身
為什么會有JMM
大致可以這么理解堂氯,并發(fā)問題的本質(zhì)是應(yīng)該串行的方法并行執(zhí)行了,并操作了不該操作的數(shù)據(jù)導(dǎo)致了錯誤的結(jié)果牌废。所以設(shè)計了悲觀鎖的機制咽白,讓對臨界區(qū)資源操作的并發(fā)程序退化成串行執(zhí)行。(理解意思即可)
所以引申出并發(fā)編程的兩個關(guān)鍵問題
第一個問題
當(dāng)我們評價一個多線程的程序時鸟缕,第一個想起的總是線程是否安全晶框,那么線程安全到底是指什么?
思考一個最常見的線程安全問題叁扫,方法A三妈,B都會訪問同樣的資源。方法A先執(zhí)行莫绣,方法B再執(zhí)行畴蒲,當(dāng)A未執(zhí)行完成B就讀取了臨界區(qū)的值,導(dǎo)致了不安全情況的發(fā)生对室。
那么精確的形容這個問題模燥,其實就是不同線程因為都存在對臨界區(qū)的操作而導(dǎo)致程序必須控制不同線程操作發(fā)生的相對順序咖祭,也就是線程的同步問題。
第二個問題
正常的多線程程序中蔫骂,一般通過共享內(nèi)存來實現(xiàn)線程之間的信息交換么翰,而這實際就是在解決并發(fā)編程的通信問題
所以多線程技術(shù)討論的核心都是這兩個問題,但這一切可以實現(xiàn)的基礎(chǔ)是要求:
先寫的代碼運行結(jié)果辽旋,之后的代碼是一定可見的
代碼的運行順序是和我們所書寫順序一樣的
但事與愿違浩嫌,簡單的認為之前寫的代碼結(jié)果一定可以被之后的代碼感知是錯誤的,因為計算機底層的復(fù)雜實現(xiàn)补胚,存在緩存码耐。寫入的代碼不一定刷到了主存中,而讀取的那一方也可能直接從緩存中讀取而不經(jīng)過主存溶其。
同樣的代碼在處理器上的最終執(zhí)行順序也并不會和書寫順序一致骚腥。
總結(jié)上面兩點,也就是:
先后運行的代碼瓶逃,多線程中不一定是內(nèi)存可見的
代碼執(zhí)行的順序一定和書寫的順序不同
而這一切其實都是底層的硬件實現(xiàn)所導(dǎo)致的束铭。所以為了,程序員可以忽略底層細節(jié)而快速方便的討論數(shù)據(jù)的內(nèi)存狀態(tài)(討論上述兩個問題)厢绝,設(shè)計出了JMM這種抽象模型契沫,它規(guī)定了Java程序運行時數(shù)據(jù)可能存在的內(nèi)存狀態(tài),也定義了在內(nèi)存級別下數(shù)據(jù)的原子性操作昔汉。同時可以看出JMM所討論的問題正是多線程技術(shù)實現(xiàn)的基礎(chǔ)埠褪。
那么在此基礎(chǔ)上來分析JMM中討論的兩個核心問題
JMM中為什么要討論內(nèi)存可見
JMM抽象示意圖如下:
從圖可以得知,如果線程A挤庇,B之間需要通信钞速,那么必須要經(jīng)歷如下兩個步驟:
線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中
線程B到主內(nèi)存中讀取線程A之前更新過的共享變量
這里也就說明了,如果希望程序正確的運行嫡秕,共享變量的內(nèi)存可見性是十分重要的(如果A修改了某個值渴语,B隨后讀取,但因為沒有將本地內(nèi)存中的值刷會主內(nèi)存昆咽,而導(dǎo)致應(yīng)該讀到的值沒有讀到)驾凶。通常情況下,從A本地內(nèi)存寫道主內(nèi)存再讀到B的本地內(nèi)存不是一個原子操作掷酗。
JMM中為什么要討論重排序
開始為了處理多線程帶來的問題调违,一般會想到讓多線程退化成單線程,也就是悲觀鎖泻轰。當(dāng)然對于悲觀鎖而言重排序沒有任何的討論意義技肩,在保證內(nèi)存可見的情況下,上一個獲得鎖對臨界區(qū)的操作一定是對下一個鎖可見的浮声,無論上一個鎖內(nèi)的指令執(zhí)行順序如何虚婿,因為對當(dāng)前獲得鎖的線程而言之前方法的操作都全部完成了順序根本沒有意義旋奢,而且JMM模型是允許在悲觀鎖內(nèi)進行重排序的。
JMM討論重排序是處于樂觀鎖的實現(xiàn)必要然痊,因為悲觀鎖性能的性能問題至朗,JUC包中的一切都是以CAS及樂觀鎖的思想進行實現(xiàn)的。這種實現(xiàn)本質(zhì)是允許多個線程同時操作臨界區(qū)的剧浸,只在關(guān)鍵步驟進行CAS操作進行檢查锹引,所以多個線程內(nèi)各自指令的操作順序就變得重要且有意義了,Java本地方法的CAS系列操作都通過內(nèi)存屏障實現(xiàn)了volatile語義的讀和寫唆香,保證了指令不被重排序樂觀鎖才有可能實現(xiàn)粤蝎。
下面的例子也可以說明重排序的問題本質(zhì)還是破壞了多線程的內(nèi)存語義,導(dǎo)致了內(nèi)存不可見袋马。
另外在假設(shè)代碼中每個讀寫操作都是原子的(也就是操作立即可見)情況下,重排序仍然會破壞內(nèi)存的可見性秸应。
代碼在實際運行時并不是按書寫的順序執(zhí)行的虑凛。為了提高性能,編譯器和處理器通常會對指令做重排序(編譯器软啼,指令級并行桑谍,內(nèi)存系統(tǒng)三種),在單線程下系統(tǒng)可以自行檢查代碼之間的依賴關(guān)系祸挪,沒有依賴關(guān)系的可以被重排序锣披。在多線程下,線程之間代碼的依賴關(guān)系顯然已經(jīng)不可能由系統(tǒng)完成贿条,觀察如下代碼雹仿。
class ReorderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; //1
flag =true; //2
}
public void reader(){
if(flag){ //3
int i = a * a;//4
}
}
}
線程B在操作4時是不一定可以看到線程A對a的寫入的。因為在線程A中1整以,2操作并沒有依賴關(guān)系胧辽,被允許重排了,而B進入判斷條件后a還沒有被賦值公黑,并無法感知到1隨后對a的修改邑商。所以即使這里保證了內(nèi)存被即使刷會主存且強制讀取,重排序還是破壞了多線程的語義凡蚜。
一些同樣重要的概念
Volatile 在JMM中有多重要
volatile規(guī)定了變量如何保證可見性人断,同時對一個變量的讀或?qū)懕WC為原子性。也就是JMM討論的核心問題之一朝蜘,內(nèi)存的可見性就是以volatile為代表恶迈,因為volatile是對于內(nèi)存可見性的最小實現(xiàn),所以討論其他操作的內(nèi)存語義時都以volatile進行比較谱醇。
而volatile的語義又是通過內(nèi)存屏障來保證的蝉绷,JMM中討論的內(nèi)存 屏障也經(jīng)過了簡化鸭廷,它對編譯器和處理器發(fā)出內(nèi)存屏障的指令,但具體實現(xiàn)取決于不同的硬件設(shè)備熔吗。
Happens-before
學(xué)習(xí)JMM難免會困惑happens-before到底是個啥辆床?舉例中總會說到volatile的寫/讀實現(xiàn)了happens-before關(guān)系,還是十分令人疑惑桅狠。
但可以明確的是Happens-before是一套形容操作間內(nèi)存可見關(guān)系的規(guī)則讼载,是JMM為了屏蔽底層的硬件細節(jié)(如重排序)而通過volatile, lock等方法和工具為程序員提供的一種便于理解內(nèi)存可見性的手段。
Happens-before定義了8種規(guī)則中跌,這些規(guī)則都是具有已有的具體實現(xiàn)上總結(jié)出的內(nèi)存可見規(guī)則咨堤,但說XXX建立了Happens-before規(guī)則就可以簡單快速的了解操作間的內(nèi)存可見情況。