Java的內存模型大概樣子還是有必要了解下的,今天就學習了下,順便學習了一點volatile關鍵字!
Java內存模型
主內存中存儲一些可以共享的變量比如實例欄位、靜態欄位和構成數組對象的元素,但是不包括局部變量與方法參數,因為它們是線程私有的,不會被共享。
每個線程獲取這類變量都是先把變量從主內存加載到工作內存,然後才能進行使用,同樣如果是修改,那麼也是先修改了工作內存中的變量,然後再從工作內存同步進主內存中。
多線程不安全
這種模式就會出現多線程不安全問題,如果兩個線程同時操作數據,如下圖:
兩個線程同時對主內存中變量進行操作,他們都會把變量先加載到自己的工作內存中去,然後對他進行操作,如果並發執行,線程2就會覆蓋線程1的操作。
關鍵字volatile
一個變量如果被volatile修飾那麼他有兩個特性:
1、變量對所有線程的可見性,意思是如果一條線程修改了這個變量的值,那麼其他線程就可以立刻知道的。
2、禁止指令重排序優化,JVM在編譯Java代碼、或者CPU在執行JVM字節碼時,對現有的字節碼指令順序進行重新排序。
特性1的可見性解釋如下圖:
volatile修飾的變量在被修改後會處理器直接將結果stroe和write進主內存,同時使得其他線程的工作內存緩存失效,這樣就實現了所謂的可見性!
volatile同樣不安全
但是如果這樣就能保證數據的安全了嗎?請看下面的示例:
30個線程都對共享變量shareVariable進行了10000次自增。但是最終的列印結果卻不是30*10000,這是因為volatile只保證了變量的可見性,並不保證變量的原子性。
通過之前的字節碼學習,我們知道「i=i++」需要用到多行字節碼指令,並且一個字節碼指令可能包含不止一條機器碼指令,因此「i=i++」並不具有原子性。所以shareVariable變量並沒有達到我們想要的結果。
也就是說即使通過volatile把變量的修改及時同步到其他線程的工作內存,但是由於對變量的操作並不是原子行,比如最簡單的「shareVariable++;」可以看下他的字節碼如下圖:
在字節碼層面也經歷了:從常量池加載i,加載常量1,然後add指令對他們計算,再put進常量池中,一共4個步驟,而機器碼就更加多了。
volatile使用場景
那麼volatile這個欄位又有什麼用呢?
由於volatile變量只能保證可見性,並不能保證原子性,不過在以下場景還是可以使用的:
1、運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
2、變量不需要與其他的狀態變量共同參與不變約束。
第一條保證了變量的準確性,第二條則是防止變量參與約束時又產生原子性問題,比如上面那個代碼「if (shareVariableBoolean)」再加一些判斷條件,新加的條件可能就會破壞原子性問題。
所以在不符合以下兩條規則的運算場景中,我們還是要通過加鎖 (使用synchronized、java.util.concurrent中的鎖或原子類)來保證原子性。
總結
今天只是簡單的了解了下Java的內存模型和volatile關鍵字,不過也知道了volatile關鍵字並不能保證線程安全,但是在面試中好像經常有這一問!
Java程式設計師日常學習筆記,如理解有誤歡迎各位交流討論!