之前在一篇文章中跟大家一同學習了CPU緩存一致性,通過緩存一致性協議MESI我們可以讓CPU各個計算核心中緩存的數據保持一致,避免造成計算結果的差異。
我們還知道Java內存模型中,各個線程還保存了一份主內存中數據的拷貝,那麼不同線程的拷貝應該如何保證數據的一致性呢,今天我們就跟大家一起看看其中的一個技術。
一、問題
有如下代碼:
代碼意思很簡單,如果看不懂的可以私信我或者+我討論。
我們重點關注work這個方法,worker在工作前需要先看看自己是否在onWork狀態,如果一個worker工作在一個線程中,那麼onWork的狀態改變我們可以確信是不會有問題的,肯定是按照指令順序執行的。
多線程狀態下會不會有問題?當然會!看如下狀態切換:
1,onWork狀態初始為false;
2,線程A將onWork狀態切換為true,但是true值還保留在線程A的局部內存中,沒有刷新回主內存;
3,線程B調用work方法,但是這個時候看到的onWork狀態還是false,所以認為自己還是下班狀態,所以不上工。
上面第3步裡面就出錯了,本來已經上班的worker,卻出現了下班狀態的輸出!
二、內存屏障
那上面的問題應該怎麼解決呢?我們分析一下問題出在哪裡。
其實很簡單,就是線程A在給onWork賦值之後沒有將最新的值從本地內存中刷回到主內存!
如果線程A更新了值,並且刷回了主內存,線程B在使用onWork之前是從主內存中讀取最新的值而不使用局部內存中的值,那麼這樣就肯定能夠避免上述問題了。
Java中也確實是這麼做的,這個技術就是內存屏障,或者叫內存柵欄,以Coder大白話理解就是當CPU遇到某個特殊變量的時候,會碰到一個柵欄,這個柵欄會攔住你繼續往下執行而讓你必須跟主內存進行一次交互,所以叫內存柵欄。這個道理是不是很簡單?但是確實解決了問題。
三、Volatile
在上面coder的大白話理解中提到CPU遇到特殊變量才有產生內存柵欄的效果,那麼這個特殊變量如何定義呢?
很簡單,就是在變量前面加上volatile關鍵字,所以上面的代碼,我們修改如下:
這樣當某個核心對onWork進行賦值之後,CPU會強制將這個值寫回主內存,在遇到要讀取onWork狀態的時候,必須從主內存中讀取最新的值。這樣就可以保證每個線程在使用的時候能夠讀取最新的狀態,這便是內存的可見性。
四、volatile的不可用場景
還記得之前我們試圖用volatile來解決同步問題嗎?代碼在這裡:
但是當在多個線程中執行同一個seller對象的時候,發現ticket並不是按照我們想像的一個個遞增的,那麼問題出在哪裡?
上面講到volatile只解決內存的可見性,並不能解決內存的原子性,看如下流程:
1,ticket初始值為0;
2,線程A調用increment將ticket的值更新為1,並且刷回主內存;
3,線程B和線程C同時調用increment方法,這個時候先從主內存讀取ticket的值,兩個線程都讀到了1,並且自增之後刷回主內存,這個時候主內存ticket的值是多少?沒錯,是2!
上面流程裡面3個線程調用了3次ticket自增,但是最後ticket的值不是3而是2。
這裡想要得到正確的值,還需要其他技術,比如同步、鎖機制,我們後面跟大家分享。
這裡簡單總結一下volatile不適用場景:volatile修飾的變量的值的更改不能依賴於前值。比如onWork的狀態更改不依賴之前到底是true還是false,但是ticket的更改因為是自增所以要依賴前值。
五、結語
今天跟大家分享了內存屏障的概念和volatile的用法,volatile可以作為輕量級的同步機制使用,但是大家一定注意其不適用的場景。關於兩個線程重量級的同步機制,這個也是面試之中常見的考點,希望大家能掌握volatile關鍵字的作用,可以私我,解答疑問,相互提升。
三人行必有我師焉!