volatile是java語言中的一個關鍵字,常用於並發編程,有兩個重要的特點:具有可見性,java虛擬機實現會為其滿足Happens before
原則;不具備原子性.用法是修飾變量,如:volatile int i
.
volatile原理
介紹其可見性先從cpu,cpu緩存和內存的關系入手.
cpu緩存是一種加速手段,cpu查找數據時會先從緩存中查找,如果不存在會從內存中查找,所以如果緩存中數據和內存中數據不一致,cpu處理數據的一致性就無法保證.從機器語言角度來講,有一些一致性協議來保證緩存一致,但是本文主要從抽象角度解釋volatile
為何能保證可見性.對於volatile
變量的賦值,會刷入主內存,並且通知其他cpu核心,你們緩存中的數據無效了,這樣所有cpu核心再想對該volatile
變量操作首先會從主內存中重新拉取值.這就保證了對於cpu操作的數據是最新.
但是這並不能保證volatile
修飾的變量的原子性.讓我們想想一個場景,變量volatile int count
存儲在內存中,cpu核心1和cpu核心2同時讀取該數據,並存入緩存,然后進行count++
操作.count++
實際可以分解為三步:
int tmp = count;
tmp = count + 1;
count = tmp;
當count = tmp
執行結束,cpu會把count
刷入內存並通知其他cpu緩存無效,如果兩個cpu核心同時將其刷入了內存,通知了緩存無效,那么我們是不是只得到了count = 2
,是不是丟失了一個+1的值.所以不要試圖用volatile
保證多步操作的原子性,原子性可以通過synchronized
進行維護.
需要注意一點,long
類型和double
類型的數據長度是64位的,JVM規范允許對於64位類型數據分開賦值,即高位32位和低位32位可以分開賦值,對於這種情況可以使用volatile
修飾保證其賦值是一次完成的.但是!!!雖然JVM是這樣規定的,絕大多數虛擬機還是實現了64位數據賦值的原子性,即使不使用volatile
關鍵字進行修飾也不會出現讀取到只賦值一半的64位類型數據,所以不必要每個long
和double
變量之前添加volatile
關鍵字.
感受一下volatile
了解完原理,來通過一段代碼感受下volatile
.
public class Volatile implements Runnable{
//自增變量i
public /*volatile*/ int i = 0;
@Override
public void run() {
while (true){
i++; //不斷自增
}
}
public static void main(String[] args) throws InterruptedException {
Volatile vt = new Volatile();
Watcher watcher = new Watcher();
watcher.v = vt;
Thread t1 = new Thread(vt);
Thread t2 = new Thread(watcher);
t1.start();
t2.start();
Thread.sleep(10);
//打印 i 和 s
System.out.println("Volatile.i = " + vt.i + "\nwatcher.w = " + watcher.monitor);
System.exit(0);
}
}
class Watcher implements Runnable{
public Volatile v;
public int monitor;
@Override
public void run() {
while (true){
monitor = v.i;//不斷將v.i的值賦給s
}
}
}
// 這是未加volatile修飾的輸出
Volatile.i = 2517483
watcher.w = 1047805
// 打開volatile注釋的輸出結果
Volatile.i = 332754
watcher.w = 333354
第一個輸出中未加volatile
修飾的i
的值和watcher
讀取的值相差太遠,
第二個輸出中相差就不多了.並且i
的值比未加volatile
關鍵字的值差很多,說明對volatile
變量的賦值消耗會大一些,不過不用在意,我們很少對volatile關鍵字進行不斷自增操作,一般都是作為狀態或者保證對象完整性,而且volatile
比synchronized
輕量太多了,如果只為了保證可見性,volatile一定是最優選.
哪些場景使用volatile
狀態變量
由於boolean
的賦值是原子性的,所以volatile
布爾變量作為多線程停止標志還簡單有效的.
class Machine{
volatile boolean stopped = false;
void stop(){stopped = true;}
}
對象完整發布
這里要提到單例對象的雙重檢查鎖,對象完整發布也依賴於happens before
原則,有興趣可以自己去查閱,這個原則是比較啰嗦,可以簡單理解為我滿足happens before
,那么我之前的代碼按順序執行.
public class Singleton {
//單例對象
private static Singleton instance = null;
//私有化構造器,避免外部通過構造器構造對象
private Singleton(){}
//這是靜態工廠方法,用來產生對象
public static Singleton getInstance(){
if(instance ==null){
//同步鎖防止多次new對象
synchronized (Singleton.class){
//鎖內非空判斷也是為了防止創建多個對象
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
這是一個會產生bug的雙重檢查鎖代碼,instance = new Singleton()
並不是一步完成的,他被分為這幾步:
1.分配對象空間;
2.初始化對象;
3.設置instance指向剛剛分配的地址。
下面圖中,線程A紅色先獲得鎖,B黃色后進入.
這種情況會出現bug,但是由於volatile
滿足happens before
原則,所以會等對象實例化之后再對地址賦值,我們需要將private static Singleton instance = null;
改成private static volatile Singleton instance = null;
即可.
其實還有幾種場景,如果想了解更多建議閱讀IBM的技術社區的文章https://www.ibm.com/developerworks/cn/java/j-jtp06197.html