【Java併發】- volatile詳解

在這之前需要先了解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變量的可見性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章