你真的瞭解volatile嗎,關於volatile的那些事

很早就接觸了volatile,可是並無特別深刻的去研究她,只有一個朦朧的概念,就是以爲java

用她來解決可見性的,但可見性又是什麼呢?安全

最近通過查閱各類資料,並結合本身的思考和實踐,對volatile有了比較深入的認識,多線程

在此總結並分享給你們。併發

可見性jvm

如何理解可見性,仍是來看個會出現死循環的例子:性能

(注意:運行時請加上jvm參數:-server,while循環內不要有標準輸出):spa

 

 這是爲何呢?先來看看java的內存模型,以下圖:線程

 

java內存分爲工做內存和主存
工做內存:即java線程的本地內存,是單獨給某個線程分配的,存儲局部變量等,同時也會複製主存的共享變量做爲本地
的副本,目的是爲了減小和主存通訊的頻率,提升效率。
主存:存儲類成員變量等server

可見性是指的是線程訪問變量是不是最新值。
局部變量不存在可見性問題,而共享內存就會有可見性問題,
由於本地線程在建立的時候,會從主存中讀取一個共享變量的副本,且修改也是修改副本,
且並非當即刷新到主存中去,那麼其餘線程並不會立刻共享變量的修改。
所以,線程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理解的誤區

不少人會認爲對volatile變量的全部操做都是原子性的,好比自增i++
這實際上是不對的。
看以下代碼:

若是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 並不會有鎖的特性

相關文章
相關標籤/搜索