前言
要想深入瞭解Java併發程式設計,就要先理解好Java記憶體模型,而要理解Java記憶體模型又不得不從硬體、計算機記憶體模型說起,本文從計算機記憶體模型產生的原因、解決的問題談起,然後再對Java模型進行介紹,最後對計算機記憶體模型和Java記憶體模型進行總結,希望大家看完本文之後有所收穫!
CPU工作過程及出現的問題
CPU執行過程
大家都知道,計算機在執行程式時,每條指令都是在CPU中執行的,而執行的時候,又免不了要和資料打交道,而計算機上面的臨時資料,是儲存在主存中的。
計算機記憶體包括快取記憶體和主存。
我們知道CPU執行指令的速度比從主存讀取資料和向主存寫入資料快很多,所以為了高效利用CPU,CPU增加了**快取記憶體(cache)**來匹配CPU的執行速度,最終程式的執行過程如下
- 首先會將資料從主存中複製一份到CPU的快取記憶體中
- 當CPU執行計算的時候就可以直接從快取記憶體中讀取資料和寫入資料
- 當運算結束後,再將快取記憶體的資料更新到主存中
快取一致性問題
上面的執行過程在單執行緒情況下並沒有問題,但是在多執行緒情況下就會出現問題,因為CPU如果含有多個核心,則每個核心都有自己獨佔快取記憶體,如果出現多個執行緒同時執行同一個操作,那麼結果是無法預知。例如2
個執行緒同時執行i++
,假設i的初始值是0
,那麼我們希望2
個執行緒執行完成之後i
的值變為2
,但是事實會是這樣嗎?
可能出現的情況有:
- 執行緒1先將
i=0
從主存中讀取到執行緒1的快取記憶體中,然後CPU完成運算,再將i=1
寫入到主存中,然後執行緒2開始從主存中讀取i=1
到執行緒2的快取記憶體中,然後CPU完成運算,再將i=2
寫入到主存中,那麼i=2
即為我們想要的結果。 - 執行緒1將
i=0
從主存中讀取到執行緒1的快取記憶體中的同時執行緒2也從主存中讀取i=0
到執行緒2的快取記憶體中,然後執行緒1和執行緒2完成運算後,也都將i=1
寫入到主存中,那麼結果i=1
,結果就不是我們想要的了。出現這個情況,我們稱為快取不一致問題。
那麼如何解決CPU出現的快取不一致問題呢?通常使用的解決方法有2種:
- 通過給匯流排加鎖
- 使用快取一致性協議
1
種方法雖然也達到了目的,但是在匯流排被鎖住的期間,其他的CPU也無法訪問主存,效率很低,所以就出現了快取一致性協議即第2
種方法,其中最出名的就是Intel
的MESI協議,MESI協議保證每個CPU快取記憶體中的變數都是一致的。它的核心思想是,當CPU寫資料時候,如果發現操作的變數是共享變數(即其他CPU上也存在該變數),就會發出訊號通知其他CPU將它快取記憶體中快取這個變數的快取行置為無效狀態,因此當其他CPU需要讀取這個變數時,發現自己快取記憶體中快取該變數的快取行為無效狀態,那麼它就會從主存中重新讀取。
處理器重排序問題
在多執行緒場景下,CPU除了會出現快取一致性問題,還會出現因為處理器重排序即處理器(CPU)為了提高效率可能會對輸入的程式碼進行亂序執行,而造成多執行緒的情況下出現問題。 例如:
//執行緒1:
context = loadContext(); //語句1
inited = true; //語句2
//執行緒2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
複製程式碼
執行緒1由於處理器重排序,先執行性了語句2,那麼此時執行緒2會認為context
已經初始化完成,那麼跳出迴圈,去執行doSomethingwithconfig(context)
方法,實際上此時context並未初始化(即執行緒1的語句1還未執行),而導致程式出錯。
什麼是計算機記憶體模型
上面提到的快取一致性問題、處理器重排序問題都是在多執行緒情況下CPU可能出現的問題,那我們應該怎麼處理這些問題?實際上這些問題並不需要我們考慮,這些問題CPU都會處理好,而CPU處理這些問題的時候是按照特定的操作規範,對特定的主存進行訪問或告訴CPU快取記憶體怎麼訪問主存,保證了多執行緒場景下的原子性、可見性、有序性,這個操作規範就稱為計算機記憶體模型。
可見性即當一個變數修改後,這個變數會馬上更新到主存中,其他執行緒會收到通知這個變數修改過了,使用這個變數的時候重新去主存獲取
什麼是Java記憶體模型
從前面的介紹瞭解到計算機記憶體模型是一種解決多執行緒場景下的一個主存操作規範,既然是規範,那麼不同的程式語言都可以遵循這種操作規範,在多執行緒場景下訪問主存保證原子性、可見性、有序性。
Java記憶體模型(Java Memory Model,JMM)即是Java語言對這個操作規範的遵循,JMM
規定了所有的變數都儲存在主存中,每個執行緒都有自己的工作區,執行緒將使用到的變數從主存中複製一份到自己的工作區,執行緒對變數的所有操作(讀取、賦值等)都必須在工作區,不同的執行緒也無法直接訪問對方工作區,執行緒之間的訊息傳遞都需要通過主存來完成。可以把這裡主存類比成計算機記憶體模型中的主存,工作區類比成計算機記憶體模型中的快取記憶體。
而我們知道JMM
其實是工作主存中的,Java記憶體模型中的工作區也是主存中的一部分,所以可以這樣說Java記憶體模型解決的是記憶體一致性問題(主存和主存)而計算機記憶體模型解決的是快取一致性問題(CPU快取記憶體和主存),這兩個模型類似,但是作用域不一樣,Java記憶體模型保證的是主存和主存之間的原子性、可見性、有序性,而計算機記憶體模型保證的是CPU快取記憶體和主存之間的原子性、可見性、有序性。
總結
本文很多觀點都是按照筆者自己的理解然後總結出來的,若有偏頗,歡迎指正!
參考
Java併發程式設計:volatile關鍵字解析
Java記憶體模型
【教程】終於有人把Java記憶體模型說清楚了!
關於JAVA記憶體模型與MESI協議?
有了快取一致性協議為什麼還需要多執行緒同步?
原文地址:https://ddnd.cn/2019/03/11/java-memory-model/