在這之前需要先了解java內存模型
volatile特性
- 可見性。對一個volatile變量的讀,總能看到(任意線程)對這個volatile變量最後的寫入。
原子性。對任意一個volatile單個變量的讀/寫具有原子性。
但是類似於volatile++這種不具有原子性。因爲volatile只保單個證讀寫具有原子性,這裏的volatile++相當於1)讀volatile的值,2)volatile + 1, 3)將更新的值賦值給volatile變量。其中包含了多個的操作,所以不具備原子性。
通俗的講,單個的volatile變量的讀/寫可以理解爲加了一個鎖
例如:
public volatile int a = 0;
public void set(){
a = 1;
}
這裏可以理解爲:
public int a = 0;
public synchronized void set(){
a = 1;
}
volatile的happens-before關係
首先需要理解重排序的概念,簡單介紹重排序:
例:
private int a = 0;
private boolean isSet = false;
public void set(){
a = 1; //1
isSet = true; //2
}
public int get(){
if(isSet) //3
return a * a; //4
}
現在假設有一個線程A先執行set,另一個線程B再執行get,當執行set的時候,在單線程中1和2的實際執行順序是不被保證的,因爲編譯器或者處理器爲了能經可能快的處理,會適當的進行指令的重排序,前提是在沒有指令依賴的情況下,但是保證執行的結果不受影響。也就是說有可能實際執行的順序是先執行2再執行1,但是放在多線程中則成了問題,當A線程先執行了2的時候,B線程看到isSet是true,這個時候去讀取了a,但是這個時候A線程中還有執行1.所以導致問題的出現。
private int a = 0;
private volatile boolean isSet = false;
public void set(){
a = 1;
isSet = true;
}
public int get(){
if(isSet)
return a * a * a * a;
return 1;
}
把變量換成volatile便不會有問題。
volatile保證在volatile變量操作之前的指令不會被重排序到volatile變量之後,volatile變量之後的指令保證不會被重排序到volatile變量操作之前。在這個例子中就是:1 happens-before 2, 3 hahappens-before 4.同時很明顯2 hahappens-before 3根據傳遞性便可以得出1 hahappens-before 3所以不會發生問題。
volatile的內存語義
- 讀:當讀一個volatile變量的時候,JMM(java內存模型)會把該線程對應的本地內存置爲無效。線程將從主存中讀取共享變量。
- 寫:當寫一個volatile變量的時候,JMM會把線程對應的本地內存的共享變量刷新到主存。
volatile變量的讀/寫都會去鎖住總線或者緩存,就像是加了一把鎖,說白了就是少去了線程自身的變量副本的讀寫的操作,直接去讀寫主存。所以內存是可見的。
volatile使用場景
以經典的雙重檢查鎖爲例。
看一個簡單的單例模式(懶漢式):
public class Singleton {
private Singleton instance = null;
private Singleton(){}
public Singleton getInstance(){
if(instance == null) //1
instance = new Singleton(); //2
return instance;
}
}
這個單例模式在單線程中是沒有問題的,但是如果是多線程都在調用getInstance方法的時候,便會有問題。原因在執行2的過程可以分爲三步:1)分配對象的內存空間 2)初始化對象 3)將instance引用指向分配的內存地址。同時這三個操作還可能被重排序爲:1)分配對象的內存空間 2)將instance引用指向分配的內存地址 3)初始化對象。當A線程執行到1的的時候B線程執行到2,也就是對象還沒有完成初始化,A線程就會進一步也去執行2,從而失去了單例模式的意義。
當然最簡單的辦法就是在getInstance方法上加上synchronized關鍵字變成同步代碼塊。但是如果有很多線程都調用getInstance方法的話synchronized對於性能的開銷很大,所以就出現了雙重檢查鎖的寫法:
private DoubleCheckedLocking instance = null;
private DoubleCheckedLocking(){}
public DoubleCheckedLocking getInstance(){
if(instance == null){
synchronized(DoubleCheckedLocking.class){
if(instance == null)
instance = new DoubleCheckedLocking(); //1
}
}
return instance;
}
}
這個方案近乎Perfect,但是有一點,那就是當A線程執行到1的時候,B線程判斷instance不爲null所以直接返回instance,但是由於上面說的編譯重排序的問題,可能導致instance確實存了對象的內存地址,但是這個時候這個對象還沒有完成初始化。
問題的根源就出在了對於instance變量的寫因爲重排序而導致沒有內存可見性,因爲使用volatile:
public class DoubleCheckedLocking {
private volatile DoubleCheckedLocking instance = null;
private DoubleCheckedLocking(){}
public DoubleCheckedLocking getInstance(){
if(instance == null){
synchronized(DoubleCheckedLocking.class){
if(instance == null)
instance = new DoubleCheckedLocking();
}
}
return instance;
}
}
這樣volatile保證volatile變量的寫happens-before volatile變量的讀,就沒有問題了。
相當於在instance的寫處加了一個鎖:
public class DoubleCheckedLocking {
private volatile DoubleCheckedLocking instance = null;
private Object lock = new Object();
private DoubleCheckedLocking(){}
public DoubleCheckedLocking getInstance(){
if(instance == null){
synchronized(DoubleCheckedLocking.class){
if(instance == null){
synchronized(lock){
instance = new DoubleCheckedLocking();
}
}
}
}
return instance;
}
}
正確使用volatile
volatile很容易被誤解爲是原子類型,或者被誤認爲和synchronized一樣,而且效率更高。實際上volatile只是保證對於volatile變量的讀/寫具有原子性,但是對於複合型操作(例如:volatile++)不保證原子性。所以volatile變量如果不是很瞭解的話慎用。
下面簡單總結使用場景:
- 對字段的寫操作不依賴於當前值。(例如:volatile++則不符合)
- 只有一個線程在寫這個volatile變量,多個線程讀的情況可以使用。
- 用作標誌位(valotile boolean flag)
聲明一個volatile的引用變量,不能保證通過該引用變量訪問到的非volatile變量的可見性。