java volatile關鍵字

java volatile關鍵字

因爲在很多代碼裏總是看到這個關鍵字,但又沒有具體的瞭解下,所以在這裏梳理一下。

首先看一個經典例子

可見性例子:

package com.study.juc.testvolatile;

/**
 * Created by yangqj on 2017/8/26.
 */
public class TestVolatile {
    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        new Thread(threadTest).start();

        while (true){
            if(threadTest.isFlag()){
                System.out.println("flag的值被修改了");
                break;
            }
        }
    }
}

class ThreadTest implements Runnable{
    private boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag="+isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

}

代碼很簡單,就是主線程和子線程共同訪問同一個變量flag,子線程修改了flag的值,然後主線程循環判斷flag的值修改了,就break 結束了。

正常邏輯下應該是期望輸出:

flag的值被修改了
flag=true

可是運行後發現結果爲:

flag=true

可以看到子線程明明修改了flag的值,但主線程還是並沒有輸出“flag的值被修改了”主線程也一直未結束,進入了死循環。

此時我們在給flag再上volatile關鍵字:

private volatile boolean flag = false;

然後再運行後發現程序輸出正常結果。爲什麼呢?

說明

計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU裏面就有了高速緩存。

也就是,當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。(以上文字拷貝網上)

Java內存模型並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題。

總結以上,得出有兩塊內存,一個是主存,一個是CPU自己的告訴緩存,也就是工作內存。我們畫個圖
image

我們上面做的異常的例子的主要原因就是這個工作內存和主內存的值不一致導致的。
整個流程爲:

1.主線程讀取主內存的flag爲false,主線程的工作內存的flag爲false。邏輯一直執行while循環
2.子線程修改子線程的工作內存中的flag爲true,並且將主內存的flag設置爲true。(特地將子線程睡眠300就是爲了讓子線程在主線程讀取了falg爲false後再執行,爲了放大問題)
3.主線程因爲while循環的高效,並沒有時間去讀取主內存的flag,一直讀取工作內存中的flag,所以一直讀取的值爲false。所以一直循環下去,導致此現象。

而volatile關鍵字有一個特性就是保證了實例變量在多個線程之間的可見性。通過volatile關鍵字,強制的從主內存中讀取變量的值。執行邏輯如圖:
image

如圖所示,線程主題讀取的直接是主內存,所以主線程讀取到的值肯定是最新的,所以加了volatile之後,正常正常運行。但也因爲放棄了工作內存,執行效率上會有一定的下降。
這個現象,主線程沒有第一時間讀取到最新的flag值的問題也被稱爲內存的可見性。 當多個線程操作共享數據時,彼此不可見。

原子性例子

volatile關鍵字有一個致命的缺點就是不支持原子性
請看例子:

package com.study.juc.testvolatile;

import com.thread.PCOne2Many.Run;

/**
 * Created by yangqj on 2017/8/26.
 */
public class TestAtomic {
    public static void main(String[] args) {
        AtomicDemo atimoc = new AtomicDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(atimoc).start();
        }
    }


}

class AtomicDemo implements Runnable{
    private int count =0;
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getCount());
    }

    public int getCount(){
        return count++;
    }
}

程序邏輯也是很簡單:
主線程開啓了10個子線程,10個子線程都去讀取一遍AtimocDemo的count值,然後+1.
正常情況下,應該是輸出0-9
然而運行結果如下:

0
2
4
0
5
1
0
3
6
7

因爲count++的操作並不是原子的
1.需要從內存中讀取count值
2.把count加1
3.再更新內存中的count值

從結果中看到有3個0輸出,也就是說
1.當1線程讀取到count值爲0 ,打印爲0,並+1後更新主存
2.在1線程未更新主存之前,有另外兩個線程2和3線程也讀取了原先主存中count的值爲0,並且打印了。所以此時3個線程都將主內存的count值設置爲1。

這就是原子性,而volatile 關鍵字不具有互斥性,多個線程可以同時訪問,所以也就不能保證變量的原子性

如果要保證其原子性,可以使用相較於synchronized,保證了互斥性,也就保證了原子性。也可以使用java.util.concurrent.atomic包下的常用原子變量。

最後將關鍵字volatile和synchronized比較一下:
1.關鍵字volatile是線程同步的輕量級實現,所以volatile性能肯定比synchronized要好,並且vilatile只能修飾於變量,而synchronized可以修飾方法,已經代碼塊。
2.多線程訪問volatile不會發生阻塞,而synchronized會出現阻塞。
3.volatile能保證數據的可見性,但不能保證數據的原子性;而synchronized可以保證原子性,也可以間接保證可見性,因爲它會將工作內存和主內存的數據做同步。
4.volatile解決的是變量在多個線程之間的可見性,而synchronized關鍵字解決的是多個線程之間訪問資源的同步性。

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