Java內存模型與volatile關鍵字

Java內存模型(Java Memory Model)

Java內存模型(JMM),不同於Java運行時數據區,JMM的主要目標是定義程序中各個變量的訪問規則,
即在虛擬機中將變量存儲到內存和從內存中讀取數據這樣的底層細節。
JMM規定了所有的變量都存儲在主內存中,但每個線程還有自己的工作內存,
線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝。
線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量,工作內存是線程之間獨立的,
線程之間變量值的傳遞均需要通過主內存來完成。

volatile關鍵字

平時在閱讀jdk源碼的時候,經常看到源碼中有寫變量被volatile關鍵字修飾,
但是卻不是十分清除這個關鍵字到底有什麼用處,現在終於弄清楚了,那麼我就來講講這個volatile到底有什麼用吧。

當一個變量被定義爲volatile之後,就可以保證此變量對所有線程的可見性,即當一個線程修改了此變量的值的時候,
變量新的值對於其他線程來說是可以立即得知的。
可以理解成:對volatile變量所有的寫操作都能立刻被其他線程得知。
但是這並不代表基於volatile變量的運算在併發下是安全的,因爲volatile只能保證內存可見性,卻沒有保證對變量操作的原子性。

比如下面的代碼:

/**
* 發起20個線程,每個線程對race變量進行10000次自增操作,如果代碼能夠正確併發,
* 則最終race的結果應爲200000,但實際的運行結果卻小於200000。
*
* @author ChenYanwei
*
*/
public class VolatileTest {
public static volatile int race = 0;

public static void increase() {
    race++;
}

private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];

    for (int i = 0; i < THREADS_COUNT; i++) {
        threads[i] = new Thread(new Runnable() {

            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    increase();
                }
            }
        });
        threads[i].start();
    }

    while (Thread.activeCount() > 1)
        Thread.yield();

    System.out.println(race);
}
}
這便是因爲race++操作不是一個原子操作,導致一些線程對變量race的修改丟失。若要使用volatale變量,一般要符合以下兩種場景:

變量的運算結果並不依賴於變量的當前值,或能夠保證只有單一的線程修改變量的值。
變量不需要與其他的狀態變量共同參與不變約束。

使用volatile變量還可以禁止JIT編譯器進行指令重排序優化,

這裏使用單例模式來舉個例子:

/**
* 單例模式例程一
*
* @author ChenYanwei
*
*/
public class Singleton_1 {

private static Singleton_1 instance = null;

private Singleton_1() {
}

public static Singleton_1 getInstacne() {
    /*
     * 這種實現進行了兩次instance==null的判斷,
     * 這便是單例模式的雙檢 鎖。
     * 則直接返回,不需要再進入同步代碼。
     * 否則就開始同步線程,進入臨界區後,進行的第二次檢查是說:
     * 如果被同步的線程有一個創建了對象實例, 
     * 其它的線程就不必再創建實例了。
     */
    if (instance == null) {
        synchronized (Singleton_1.class) {
            if (instance == null) {
                /*
                 * 仍然存在的問題:下面這句代碼並不是一個原子操作,                         
                 * JVM在執行這行代碼時,會分解成如下的操作:
                 * 1.給instance分配內存,
                 * 在棧中分配並初始化爲null
                 * 2.調用Singleton_1的構造函數,生成對象實例,
                 * 在堆中分配
                 * 3.把instance指向在堆中分配的對象
                 * 由於指令重排序優化,執行順序可能會變成1,3,2,
                 * 那麼當一個線程執行完1,3之後,被另一個線程搶佔,
                 * 這時instance已經不是null了,就會直接返回。
                 * 然而2還沒有執行過,
                 * 也就是說這個對象實例還沒有初始化過。
                 */
                instance = new Singleton_1();
            }
        }
    }
    return instance;
}
}

/**
* 單例模式例程二
*
* @author ChenYanwei
*
*/
public class Singleton_2 {

/*
 * 爲了避免JIT編譯器對代碼的指令重排序優化,可以使用volatile關鍵字,
 * 通過這個關鍵字還可以使該變量不會在多個線程中存在副本,
 * 變量可以看作是直接從主內存中讀取,相當於實現了一個輕量級的鎖。
 */
private volatile static Singleton_2 instance = null;

private Singleton_2() {
}

public static Singleton_2 getInstacne() {
    if (instance == null) {
        synchronized (Singleton_2.class) {
            if (instance == null) {
                instance = new Singleton_2();
            }
        }
    }
    return instance;
}
}
變量在有了volatile修飾之後,對變量的修改會有一個內存屏障的保護,使得後面的指令不能被重排序到內存屏障之前的位置。
volalite變量的讀性能與普通變量類似,但是寫性能要低一些,因爲它需要插入內存屏障指令來保證處理器不會發生亂序執行。
即便如此,大多數場景下volatile的總開銷仍然要比鎖低,所以volatile的語義能滿足需求時候,選擇volatile要優於使用鎖。
發佈了31 篇原創文章 · 獲贊 23 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章