很早就接觸了volatile,但是並沒有特別深入的去研究她,只有一個朦朧的概念,就是覺得
用她來解決可見性的,但可見性又是什么呢?
最近經過查閱各種資料,並結合自己的思考和實踐,對volatile有了比較深刻的認識,
在此總結並分享給大家。
可見性
如何理解可見性,還是來看個會出現死循環的例子:
(注意:運行時請加上jvm參數:-server,while循環內不要有標准輸出):

這是為什么呢?先來看看java的內存模型,如下圖:

java內存分為工作內存和主存
工作內存:即java線程的本地內存,是單獨給某個線程分配的,存儲局部變量等,同時也會復制主存的共享變量作為本地
的副本,目的是為了減少和主存通信的頻率,提高效率。
主存:存儲類成員變量等
可見性是指的是線程訪問變量是否是最新值。
局部變量不存在可見性問題,而共享內存就會有可見性問題,
因為本地線程在創建的時候,會從主存中讀取一個共享變量的副本,且修改也是修改副本,
且並不是立即刷新到主存中去,那么其他線程並不會馬上共享變量的修改。
因此,線程B修改共享變量后,線程A並不會馬上知曉,就會出現上述死循環的問題。
解決共享變量可見性問題,需要用volatile關鍵字修飾。
如下圖代碼就不會出現死循環:

那么為什么能解決死循環的問題呢?
可見性的特性總結為以下2點:
1. 對volatile變量的寫會立即刷新到主存
2. 對volatile變量的讀會讀主存中的新值
可以用如下圖更清晰的描述:
如此一來,就不會出現死循環了。
為了能更深刻的理解volatile的語義,我們來看下面的時序圖,回答這2個問題:
問題1:t2時刻,如果線程A讀取running變量,會讀取到false,還是等待線程B執行完呢?
答案是否定的,volatile並沒有鎖的特性。
問題2:t4時刻,線程A是否一定能讀取到線程B修改后的最新值
答案是肯定的,線程A會從重新從主存中讀取running的最新值。
還有一種辦法也可以解決死循環的問題:
雖然running變量上沒有volatile關鍵字修飾,但是讀和寫running都是同步方法
同步塊存在如下語義:
1.進入同步塊,訪問共享變量會去讀取主存
2.退出同步塊,本地內存對共享變量的修改會立即刷新到主存
因此上述代碼不會出現死循環。
volatile變量的原子性
我看了很多文章,有些文章甚至是出版的書籍都說volatile不是原子的,
他們舉的例子是i++操作,i++本身不是原子操作,是讀並寫,我這里要講的原子性
指的是寫操作,原子性的特別總結為2點:
1. 對一個volatile變量的寫操作,只有所有步驟完成,才能被其它線程讀取到。
2. 多個線程對volatile變量的寫操作本質上是有先后順序的。也就是說並發寫沒有問題。
這樣說也許讀者感覺不到和非volatile變量有什么區別,我來舉個例子:
//線程1初始化User
User user;
user = new User();
//線程2讀取user
if(user!=null){
user.getName();
}
在多線程並發環境下,線程2讀取到的user可能未初始化完成
具體來看User user = new User的語義:
1:分配對象的內存空間
2:初始化對線
3:設置user指向剛分配的內存地址
步驟2和步驟3可能會被重排序,流程變為
1->3->2
這些線程1在執行完第3步而還沒來得及執行完第2步的時候,如果內存刷新到了主存,
那么線程2將得到一個未初始化完成的對象。因此如果將user聲明為volatile的,那么步驟2,3
將不會被重排序。
下面我們來看一個具體案例,一個基於雙重檢查的懶加載的單例模式實現:
這個單例模式看起來很完美,如果instance為空,則加鎖,只有一個線程進入同步塊
完成對象的初始化,然后instance不為空,那么后續的所有線程獲取instance都不用加鎖,
從而提升了性能。
但是我們剛才講了對象賦值操作步驟可能會存在重排序,
即當前線程的步驟4執行到一半,其它線程如果進來執行到步驟1,instance已經不為null,
因此將會讀取到一個沒有初始化完成的對象。
但如果將instance用volatile來修飾,就完全不一樣了,對instance的寫入操作將會變成一個原子
操作,沒有初始化完,就不會被刷新到主存中。
修改后的單例模式代碼如下:
對volatile理解的誤區
如果i++的操作是線程安全的,那么預期結果應該是i=20000
然而運行的結果是:11349
說明i++存在並發問題
i++語義是i=i+1
分為2個步驟
步驟1:讀取i=0
步驟2:計算i+1=1,並重新賦值給i
那么可能存在2個線程同時讀取到i=0,並計算出結果i=1然后賦值給i
那么就得不到預期結果i=2。
這個問題說明了2個問題:
1.i++這種操作不是原子操作
2.volatile 並不會有鎖的特性
