Java內存模型與volatile關鍵字

pdce 9年前發布 | 12K 次閱讀 Java內存 Java開發

原文出處: 博客(從零到無窮大)

Java內存模型(Java Memory Model)

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

volatile關鍵字

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

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

/**
 * 發起20個線程,每個線程對race變量進行10000次自增操作,如果代碼能夠正確并發,
 * 則最終race的結果應為200000,但實際的運行結果卻小于200000。
 * 
 * @author Colin Wang
 *
 */
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變量,一般要符合以下兩種場景:

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

使用volatile變量還可以禁止JIT編譯器進行指令重排序優化,這里使用單例模式來舉個例子:

/**
 * 單例模式例程一
 * 
 * @author Colin Wang
 *
 */
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 Colin Wang
 *
 */
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要優于使用鎖。

 本文由用戶 pdce 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!