之前提到的原子性、可見性、有序性都與Java內(nèi)存模型(JMM)密不可分。在Java內(nèi)存模型中定義了主內(nèi)存和線程的工作內(nèi)存的概念,還有8個原子性操作。這些概念稍后會介紹,我現(xiàn)在想說的是,為什么會出現(xiàn)JMM
從硬件的角度看并發(fā)問題
當程序在運行的時候,從硬件的執(zhí)行上來看,就是CPU從內(nèi)存中取數(shù)據(jù) => 計算 => 把數(shù)據(jù)寫回內(nèi)存
CPU的執(zhí)行速度是很快的,可是內(nèi)存比CPU慢了好幾個數(shù)量級,為了提升效率,就在CPU和內(nèi)存之間,加了緩存。(實際的結(jié)構比我描述的要復雜的多,CPU訪問寄存器的速度是最快的,其次是緩存,緩存又分為多級緩存,最慢的是內(nèi)存。那有人可能會問,把緩存做的跟內(nèi)存一樣大,替換掉內(nèi)存不就好了?為什么不這么做呢,因為緩存太貴!)
硬件結(jié)構.png
從圖中我們能看到,每個CPU都有自己的一塊緩存。比如對一個變量做操作,首先會查看緩存中是不是存在這個變量,如果存在,就直接從緩存中??;如果不存在,去內(nèi)存中取。當CPU修改了某個變量之后,不會把這個變量的值立刻寫會內(nèi)存,會在一個合適的時候?qū)懟氐絻?nèi)存(這個合適的時候是不可控的,下面要講的緩存一致性協(xié)議可以強制要求寫回到內(nèi)存)。這其中的并發(fā)問題應該就很明顯了。兩個CPU的緩存中同時緩存了同一個變量 i ,其中一個把 i 的值改為5,但這時另一個CPU不知道有人已經(jīng)修改了 i 的值,依舊用的緩存中變量 i 的值來做計算。
緩存一致性協(xié)議
如何解決這種問題呢?主要有兩種方案來解決緩存一致性問題
總線加鎖
緩存一致性協(xié)議(MESI)
首先來看第一種加鎖的方案就比較簡單粗暴了,簡單來說就是CPU(1) 在對內(nèi)存中的一個變量進行操作的過程中,其他的CPU如果也想對這個變量進行某些操作,只能等到CPU(1) 操作完成之后才有機會操作這個變量(期間可能會有多個CPU在等待,這些等待的CPU只會有一個能得到操作這個變量的機會,其他的CPU則繼續(xù)等待)。這樣看來,效率會非常的低下。
第二種方案是通過緩存一致性協(xié)議來解決。緩存一致性協(xié)議有很多種,MESI是比較常用的一種,其中定義了緩存行的四種狀態(tài)(M:修改,E:獨占,S:共享,I:失效;其底層原理可以查閱相關文章)。作用就是,多個CPU的緩存中都存有變量 x ,當其中一個CPU修改了變量 x 的值之后,其他CPU緩存中的值就失效了,再次使用變量 x 就去內(nèi)存中取值。
緩存一致性.png
Java內(nèi)存模型
看過了硬件的模型之后,與下圖中的Java內(nèi)存模型相比較,幾乎差不多。
Java內(nèi)存模型是Java虛擬機規(guī)范中定義的,用來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異。我來解釋一下這句話。首先,Java虛擬機規(guī)范,就是規(guī)定Java虛擬機能夠做哪些事情,至于通過什么方式來做這些事情,要交給具體的虛擬機廠商來實現(xiàn)了(虛擬機的是實現(xiàn)由很多,我們常用的是HotSpot)。后半句話的意思是,我們目前使用最多的有三種操作系統(tǒng),Windows、Linux、Unix,CPU也分為很多種,它們之間交互操作的命令和訪問內(nèi)存的方式也有區(qū)別,Java作為一種跨平臺的編程語言,自然要屏蔽掉這些差異(可以類比一下接口和實現(xiàn)類。Java內(nèi)存模型就相當于接口,底層的各種硬件和操作系統(tǒng)相當于實現(xiàn)類。不管有多少個實現(xiàn)類,總歸會提供接口中定義的方法。作為用戶,只需要了解接口中的方法如何使用即可,不需要關心底層的實現(xiàn))。
所以說,Java只管定義出自己的一套內(nèi)存交互的方式,適用于各種的硬件和操作系統(tǒng),具體怎么樣屏蔽掉這些差異就交給虛擬機廠商來實現(xiàn)。
Java內(nèi)存模型.png
從圖中我們可以看出,Java內(nèi)存模型的定義與真實的物理模型類似。
在Java內(nèi)存模型中,定義了8種原子操作,規(guī)定了這8種操作一定是原子性操作。為什么要定義這8種操作呢?我們知道,Java內(nèi)存模型是對于物理機底層的抽象,對于同一個操作來說,不同的物理機的實現(xiàn)可能會有差異。所以,Java內(nèi)存模型才會定義這8種操作來達到統(tǒng)一性。
再談原子性
Java內(nèi)存模型中定義了8種原子操作,跟我們在代碼中強調(diào)的原子性有什么區(qū)別呢?
從圖中可以看出,從內(nèi)存中取值,賦值,再寫回內(nèi)存被拆分成了6個操作,所以,我們想要保證這6個操作具有原子性,就需要用lock,unlock來保證。不過Java沒有直接提供給我們這種使用方式,而是提供了synchronized關鍵字,接下來的文章我會再來分析synchronized的用法。
可見性
現(xiàn)在回過頭來看可見性的問題,是不是跟硬件中的緩存一致性問題很像啊。Java提供了volatile關鍵字來保證被修飾變量的可見性。那么現(xiàn)在就有個問題了,既然在硬件上都已經(jīng)保證了可見性了,那為什么還要在代碼中提供一種保證可見性的方式呢?
我們來分析一下,是不是每個變量都需要保證可見性呢?還有就是,從硬件角度上來看,為了保證緩存一致性會損失掉多少性能呢?
第一個問題,顯然是不需要保證每個變量的可見性的,比如非并發(fā)的場景下,或者局部變量。
第二個問題,緩存的出現(xiàn),就是為了避免CPU頻繁的和內(nèi)存直接交互,因為CPU的執(zhí)行速度比內(nèi)存的讀寫速度要快上百倍。現(xiàn)象一下,如果每個變量都要保證可見性的話,那么緩存的作用就很小了,CPU與內(nèi)存頻繁交互,會損失掉大量的計算資源。
所以,硬件的緩存一致性協(xié)議是通過某些條件觸發(fā)或者會對某個具有特殊標記的符號修飾的變量進行緩存一致性的處理。那么,Java提供的volatile關鍵字就會給被修飾的變量加上特殊的標記,在CPU執(zhí)行的過程中,就會對這個變量做特殊處理。


