volatile和synchronized的區別

預備知識

Java內存模型

首先我們來了解一下JMM(java內存模型)
  java虛擬機有自己的內存模型(Java Memory Model,JMM),JMM可以屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果。
  JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在本地內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關係如下:
JMM內存模型圖
  需要注意的是,JMM是個抽象的內存模型,所以所謂的本地內存,主內存都是抽象概念,並不一定就真實的對應cpu緩存和物理內存。當然如果是出於理解的目的,這樣對應起來也無不可。

原子性

原子性意味着一個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。

可見性

可見性則更爲微妙,它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的。如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。

舉個栗子

爲了能徹底的理解volatile,我們一步一步來分析。首先來看看如下代碼

public class TestVolatile {
    boolean status = false;

    /**
     * 狀態切換爲true
     */
    public void changeStatus(){
        status = true;
    }

    /**
     * 若狀態爲true,則running。
     */
    public void run(String t){
        if(status){
            System.out.println("running...." + t);
        }
    }
}

上面這個例子,在多線程環境裏,假設線程A執行changeStatus()方法後,線程B運行run()方法,可以保證輸出"running…"嗎?
  答案是NO!
  這個結論會讓人有些疑惑,可以理解。因爲倘若在單線程模型裏,先運行changeStatus方法,再執行run方法,自然是可以正確輸出"running…"的;但是在多線程模型中,是沒法做這種保證的。因爲對於共享變量status來說,線程A的修改,對於線程B來講,是"不可見"的。也就是說,線程B此時可能無法觀測到status已被修改爲true。那麼什麼是可見性呢?
  所謂可見性,是指當一條線程修改了共享變量的值,新值對於其他線程來說是可以立即得知的。很顯然,上述的例子中是沒有辦法做到內存可見性的。
  在瞭解了JMM的簡單定義後,問題就很容易理解了,對於普通的共享變量來講,比如我們上文中的status,線程A將其修改爲true這個動作發生在線程A的本地內存中,此時還未同步到主內存中去;而線程B緩存了status的初始值false,此時可能沒有觀測到status的值被修改了,所以就導致了上述的問題。那麼這種共享變量在多線程模型中的不可見性如何解決呢?比較粗暴的方式自然就是加鎖,但是此處使用synchronized或者Lock這些方式太重量級了,有點炮打蚊子的意思。比較合理的方式其實就是volatile。

volatile

volatile具備兩種特性,第一就是保證共享變量對所有線程的可見性。將一個共享變量聲明爲volatile後,會有以下效應:
  
  1、當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存中去;
   2、這個寫會操作會導致其他線程中的緩存無效。

上面的例子只需將status聲明爲volatile,即可保證在線程A將其修改爲true時,線程B也可以得知。

使用場景

先說一下volatile的使用場景,你只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:
1、對變量的寫操作不依賴於當前值。
2、該變量沒有包含在具有其他變量的不變式中。
volatile最適用一個線程寫,多個線程讀的場合。
如果有多個線程併發寫操作,仍然需要使用鎖或者線程安全的容器或者原子變量來代替。

再舉栗子

代碼如下:

public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch來等待計算線程執行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //開啓30個線程進行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待計算線程執行完
        countDownLatch.await();
        System.out.println(num);
    }
}

針對這個示例,一些同學可能會覺得疑惑,如果用volatile修飾的共享變量可以保證可見性,那麼結果不應該是300000麼?
問題就出在num++這個操作上,因爲num++不是個原子性的操作,而是個複合操作。我們可以簡單講這個操作理解爲由這三步組成:
  1.讀取
  2.加一
  3.賦值
  所以,在多線程環境下,有可能線程A將num讀取到本地內存中,此時其他線程可能已經將num增大了很多,線程A依然對過期的num進行自加,重新寫到主存中,最終導致了num的結果不合預期,而是小於30000。這時候就需要使用synchronized了。

synchronized

當它用來修飾一個方法或者一個代碼塊的時候,能夠保證在同一時刻最多隻有一個線程執行該段代碼。

  1. 當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。
  2. 然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
  3. 尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
  4. 當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就獲得了這個object的對象鎖。結果,其它線程對該object對象所有同步代碼部分的訪問都被暫時阻塞。

兩者的區別

1、volatile是變量修飾符,而synchronized則作用於一段代碼或方法。
2、volatile只是在線程內存和“主”內存間同步某個變量的值;而synchronized通過鎖定和解鎖某個監視器同步所有變量的值, 顯然synchronized要比volatile消耗更多資源。
3、volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。
4、volatile保證數據的可見性,但不能保證原子性;而synchronized可以保證原子性,也可以間接保證可見性,因爲它會將私有內存中和公共內存中的數據做同步。
5、volatile標記的變量不會被編譯器優化;synchronized標記的變量可以被編譯器優化。

線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。

關鍵字volatile主要使用的場合是在多個線程中可以感知實例變量被修改,並且可以獲得最新的值使用,也就是多線程讀取共享變量時可以獲得最新值使用。

關鍵字volatile提示線程每次從共享內存中讀取變量,而不是私有內存中讀取,這樣就保證了同步數據的可見性。如上第二個栗子,可以得出volatile本身並不處理數據的原子性,而是強制對數據的讀寫及時的影響到主內存中。

最後修改的代碼

還未修改,晚了,明天再改

public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch來等待計算線程執行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //開啓30個線程進行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待計算線程執行完
        countDownLatch.await();
        System.out.println(num);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章