閱讀本文需要熟悉java內存模型:Java內存模型-JMM解析
1.前言
java內存模型規定了所有的變量都存儲主內存,注意這裏的主內存,是依附在物理上的內存,jvm
就運行在物理內存上(我的電腦是16G),所以這裏的主內存就是虛擬機的一部分,除此之外,每條線程還有自己的工作內存(本地內存),工作內存是JMM
的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化線程的,工作內存中保存了被該線程使用的在主內存中的變量副本,線程對變量的讀取賦值等操作都必須在工作內存中進行,不能直接在主內存中進行讀寫
2.volatile的出現及特性
無論是concurrent
包下的原子性操作還是synchronized
同步機制都是確保共享變量(主內存裏的變量) 在多線程條件下共享變能夠在線程間交互,通信,保障其安全性,都是基於內存層面進行操作,而volatile
同樣是Java虛擬機提供的一種輕量級的同步機制,下面說說它的特徵
- 可見性:使它修飾的變量在線程間可見,可見指的是當一個線程修改了這個變量的值,新值對其他線程來說是可以立刻得知
- 不保證原子性:對於類似++的自增操作,volatile不能確保其在多線程條件下的安全性,因此用原子類代替是一個可選的方案
3.volatile讀寫與happens-before的關係
簡述一下happens-before
的特點(詳解請看開頭文章鏈接):
- 如果一個操作與另一個操作有Happens-Before關係,那麼第一個操作將對第二個操作可見,且第一個操作的順序要在第二個操作之前
在java虛擬機規定的幾種happens-before
原則中,volatile
屬於其中一種,我們看一下它的內存定義:
- 對volatile變量寫時的內存定義:當對一個volatile變量進行寫操作時,JMM會把該線程內的工作內存中的共享變量刷新到主內存
- 對volatile變量讀時的內存定義:當對一個volatile變量進行讀操作時,JMM會把本線程內工作內存中的共享變量置爲無效,從主內存去讀取共享變量
這也是爲什麼volatile
具有可見性的原因
4.volatile讀寫內存定義實現,避免指令重排
在此之前需要了解一下指令重排序,文章頭部有鏈接,volatile實現可見性還有一個重要原因:
避免指令重排
從表格可以看出:
- 當第一個操作爲普通變量的讀寫時,如果第二個操作時時volatile寫,則不能把這兩個指令重排序
- 當第二個操作爲volatile寫,無論第一個操作是什麼類型,都不能進行重排序
- 當第一個操作爲volatile讀時,無論第二個操作是什麼類型,都不能進行重排序
- 當第一個操作爲volatile寫第二個操作爲volatile讀時,不能進行重排序
這個表格就是對volatile
關鍵字的讀寫定義,即在這種機制下,保證了共享變量的可見性和安全性,爲了實現這個定義,java編譯器在實現代碼時會在代碼中加入內存屏障來禁止處理器(cpu)的指令重排,具體實現如下:
- 在每個volatile讀後插入LoadLoad屏障
- 在每個volatile讀後插入LoadStore屏障
- 在每個volatile寫前插入StoreStore屏障
- 在每個volatile寫後插入StoreLoad屏障
我們通過一個例子來說明:
public class Demo {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite(){
int i = v1;//第一個volatile讀
int j = v2;//第二個volatile讀
a = i + j;//普通寫
v1 = i + 1;//第一個volatile寫
v2 = j * 2;//第一個volatile寫
}
}
針對上面的代碼,編譯器在生成字節碼時可以做到如下優化
通過這種方式就可以保障volatile的happens-before
規則,很好的避免了指令重排序