天道不一定酬所有勤
但是,天道只酬勤

深入理解Java中的volatile關鍵字

再有人問你Java內存模型是什么,就把這篇文章發給他中我們曾經介紹過,Java語言為了解決并發編程中存在的原子性、可見性和有序性問題,提供了一系列和并發處理相關的關鍵字,比如synchronized、volatile、final、concurren包等。在前一篇文章中,我們也介紹了synchronized的用法及原理。本文,來分析一下另外一個關鍵字——volatile。

本文就圍繞volatile展開,主要介紹volatile的用法、volatile的原理,以及volatile是如何提供可見性和有序性保障的等。

volatile這個關鍵字,不僅僅在Java語言中有,在很多語言中都有的,而且其用法和語義也都是不盡相同的。尤其在C語言、C++以及Java中,都有volatile關鍵字。都可以用來聲明變量或者對象。下面簡單來介紹一下Java語言中的volatile關鍵字。

volatile的用法

volatile通常被比喻成”輕量級的synchronized“,也是Java并發編程中比較重要的一個關鍵字。和synchronized不同,volatile是一個變量修飾符,只能用來修飾變量。無法修飾方法及代碼塊等。

volatile的用法比較簡單,只需要在聲明一個可能被多線程同時訪問的變量時,使用volatile修飾就可以了。

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

如以上代碼,是一個比較典型的使用雙重鎖校驗的形式實現單例的,其中使用volatile關鍵字修飾可能被多個線程同時訪問到的singleton。

volatile的原理

再有人問你Java內存模型是什么,就把這篇文章發給他中我們曾經介紹過,為了提高處理器的執行速度,在處理器和內存之間增加了多級緩存來提升。但是由于引入了多級緩存,就存在緩存數據不一致問題。

但是,對于volatile變量,當對volatile變量進行寫操作的時候,JVM會向處理器發送一條lock前綴的指令,將這個緩存中的變量回寫到系統主存中。

但是就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題,所以在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議

緩存一致性協議:每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操作的時候,會強制重新從系統內存里把數據讀到處理器緩存里。

所以,如果一個變量被volatile所修飾的話,在每次數據變化之后,其值都會被強制刷入主存。而其他處理器的緩存由于遵守了緩存一致性協議,也會把這個變量的值從主存加載到自己的緩存中。這就保證了一個volatile在并發編程中,其值在多個緩存中是可見的。

volatile與可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

我們在再有人問你Java內存模型是什么,就把這篇文章發給他中分析過:Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。所以,就可能出現線程1改了某個變量的值,但是線程2不可見的情況。

前面的關于volatile的原理中介紹過了,Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。

volatile與有序性

有序性即程序執行的順序按照代碼的先后順序執行。

我們在再有人問你Java內存模型是什么,就把這篇文章發給他中分析過:除了引入了時間片以外,由于處理器優化和指令重排等,CPU還可能對輸入代碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是可能存在有序性問題。

volatile除了可以保證數據的可見性之外,還有一個強大的功能,那就是他可以禁止指令重排優化等。

普通的變量僅僅會保證在該方法的執行過程中所依賴的賦值結果的地方都能獲得正確的結果,而不能保證變量的賦值操作的順序與程序代碼中的執行順序一致。

volatile可以禁止指令重排,這就保證了代碼的程序會嚴格按照代碼的先后順序執行。這就保證了有序性。被volatile修飾的變量的操作,會嚴格按照代碼順序執行,load->add->save 的執行順序就是:load、add、save。

volatile與原子性

原子性是指一個操作是不可中斷的,要全部執行完成,要不就都不執行。

我們在Java的并發編程中的多線程問題到底是怎么回事兒?中分析過:線程是CPU調度的基本單位。CPU有時間片的概念,會根據不同的調度算法進行線程調度。當一個線程獲得時間片之后開始執行,在時間片耗盡之后,就會失去CPU使用權。所以在多線程場景下,由于時間片在線程間輪換,就會發生原子性問題。

在上一篇文章中,我們介紹synchronized的時候,提到過,為了保證原子性,需要通過字節碼指令monitorentermonitorexit,但是volatile和這兩個指令之間是沒有任何關系的。

所以,volatile是不能保證原子性的。

在以下兩個場景中可以使用volatile來代替synchronized

1、運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程會修改變量的值。

2、變量不需要與其他狀態變量共同參與不變約束。

除以上場景外,都需要使用其他方式來保證原子性,如synchronized或者concurrent包。

我們來看一下volatile和原子性的例子:

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保證前面的線程都執行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

以上代碼比較簡單,就是創建10個線程,然后分別執行1000次i++操作。正常情況下,程序的輸出結果應該是10000,但是,多次執行的結果都小于10000。這其實就是volatile無法滿足原子性的原因。

為什么會出現這種情況呢,那就是因為雖然volatile可以保證inc在多個線程之間的可見性。但是無法inc++的原子性。

總結與思考

我們介紹過了volatile關鍵字和synchronized關鍵字?,F在我們知道,synchronized可以保證原子性、有序性和可見性。而volatile卻只能保證有序性和可見性。

那么,我們再來看一下雙重校驗鎖實現的單例,已經使用了synchronized,為什么還需要volatile?

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  

答案,我們在下一篇文章:既生synchronized,何生亮volatile中介紹,敬請關注我的博客(http://www.057299.live)和公眾號(Hollis)。

(全文完) 歡迎關注『Java之道』微信公眾號
贊(25)
如未加特殊說明,此網站文章均為原創,轉載必須注明出處。HollisChuang's Blog » 深入理解Java中的volatile關鍵字
分享到: 更多 (0)

評論 2

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
  1. #1

    這篇分析還是看的暈乎乎的,沒有搞懂為什么volatile關鍵字不能保證線程安全,只說它不能保證原子性是不能說服我的。樓主說的volatile關鍵字修飾的變量在線程修改完后會刷新到主內存中,其他線程在使用的時候就要從主內存中重新刷新到線程工作內存中。能用線程運行時候的內存分配舉例嗎。我理解的volatile不能保證線程安全原因:不加關鍵字時候線程1是從主內存中拷貝變量副本到線程1的工作內存中,線程在工作內存中修改變量后,當線程執行完畢時候再把工作內存中變臉拷貝副本刷新到主內存中。加了volatile關鍵字后不等線程執行完畢修改變量后就直接刷新到主內存中主內存也會把變量刷新到另一個線程2工作內存中,但是如果這時候如果線程2在自己的工作內存已經執行讀取變量值話那么線程1中的刷新過來的變量值就不能被線程2讀取啦,線程2修改完后更新在自己工作內存中變量拷貝副本,再刷新到主內存中去。也許就是不能保證原子性導致的吧。

    三國諸葛亮丞相2年前 (2018-08-22)回復
    • 我也是啊,好像只是說變量,但是到底為什么呢??

      不二易7個月前 (08-27)回復

HollisChuang's Blog

聯系我關于我
大乐透复式返奖 微乐麻将下载手机版 星悦内蒙麻将一口香 北京赛车app苹果下载 北京11远5走势图一定牛 历届英超冠军一览表 欢乐谷棋牌游戏中心 青海快3开奖结果今天海快三开奖走势图 股市微信群是骗局揭秘 吉祥棋牌官网免费下载 什么叫私募基金