volatile原理和應用場景


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位類型數據,所以不必要每個longdouble變量之前添加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關鍵字進行不斷自增操作,一般都是作為狀態或者保證對象完整性,而且volatilesynchronized輕量太多了,如果只為了保證可見性,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM