java併發:java記憶體模型與多執行緒之volatile
java記憶體模型
Java作為平臺無關性語言,JSL(java語言規範)定義了一個統一的記憶體管理模型JMM(Java Memory Model),JMM遮蔽了底層平臺記憶體管理細節。
JMM規定了JVM有主記憶體(Main Memory)和工作記憶體(Working Memory)。
主記憶體
即java堆記憶體,存放程式中所有的類例項、靜態資料等變數,是多個執行緒共享的。
工作記憶體
用於存放執行緒從主記憶體中拷貝過來的變數以及訪問方法所取得的區域性變數,是每個執行緒私有的,其他執行緒不能訪問。
Note:
執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接對主存進行操作。
於是每個執行緒對變數的操作都是先從主記憶體將其拷貝到工作記憶體,再對其進行操作
示例
在java中,執行下面這個語句:
i=10;執行執行緒必須先在自己的工作執行緒中對變數i所在的快取行進行賦值操作,然後再寫入主存當中,而不是直接將數值10寫入主存當中。
記憶體不可見問題
可見性指的是在一個執行緒中修改變數的值以後,在其他執行緒中能夠看到這個值。
Java 記憶體模型是一個抽象的概念,在實際實現中執行緒的工作記憶體是什麼呢?
為了獲得較好的執行效能,Java記憶體模型並沒有限制執行引擎使用處理器的暫存器或者快取記憶體來提升指令執行速度,也沒有限制編譯器對指令進行重排序。
Java 記憶體模型的工作記憶體對應下圖中的 Ll 或者 L2 快取或者 CPU 的暫存器。
案例
假如執行緒 A 和執行緒 B 同時處理一個共享變數 , 會出現什麼情況?
情景假設:
執行緒 A 和執行緒 B 使用不同CPU執行,並且當前兩級Cache都為空
執行過程:
- 執行緒A根據需要獲取共享變數X的值,由於兩級Cache都沒有命中,所以載入主記憶體中 X 的值(假如為 0),並把 X 的值快取到兩級快取
- 執行緒A修改 X 的值為 1,然後將其寫入兩級 Cache,並重新整理到主記憶體
- 執行緒B獲取 X 的值,一級快取沒有命中,二級快取命中了,返回X=1
- 執行緒B修改 X 的值為 2,將其存放到執行緒 2 所在的一級 Cache 和共享二級 Cache 中,最後更新主記憶體中X的值為2
- 執行緒A根據需要獲取共享變數X的值,一級快取命中,其中X=l
到這裡問題就出現了,執行緒 B 已經把 X 的值修改為了 2,執行緒 A 獲取的還是 1;這就是共享變數的記憶體不可見問題,此處體現為執行緒 B 寫入的值對執行緒 A 不可見。
據此可知,在java記憶體模型中,存在快取一致性問題問題,所以在多執行緒環境中必須解決可見性問題。
對此Java提供瞭解決方式:使用關鍵字 volatile
關鍵字volatile
volatile是一個特殊的修飾符,只有成員變數才能使用它,具體用法如下:
public volatile static int count=0;//在宣告的時候帶上volatile關鍵字即可
與Synchronized及ReentrantLock等提供的互斥相比,Synchronized保證了Synchronized同步塊中變數的可見性,而volatile則是保證了所修飾變數的可見性。
詳解
關鍵字volatile,從表面意思上是說這個變數是易變的,不穩定的。
這個關鍵字的作用是告訴編譯器,凡是被該關鍵字宣告的變數都是易變的、不穩定的;所以不要試圖對該變數使用快取等優化機制,而應當每次都從它的記憶體地址中去讀值。
Note:
這裡只是說每次讀取volatile的變數時都要從它的記憶體地址中讀取,並沒有說每次修改完volatile的變數後都要立刻將它的值寫回記憶體。
也就是說volatile只提供了記憶體可見性,而沒有提供原子性。
同一個變數多個執行緒間的可見性與多個執行緒中操作互斥是兩件事情,操作互斥提供了操作整體的原子性,所以說如果用這個關鍵字做高併發的安全機制的話是不可靠的。
問題:什麼時候使用volatile關鍵字?
根據volatile的特點,其最好用於對記憶體可見性要求高,而對原子性要求低的地方,譬如:用於修飾作為開關狀態的變數。