volatile關鍵字

提示:閱讀這篇文章的時候最好先掌握Java內存模型(JMM)的相關內容,不然可能會感到不適。

大多數(shù)人接觸到這個關鍵字都是在學習單例模式的時候,他可以保證在并發(fā)的場景下不會產生多個實例對象的情況。

通常volatile用來修飾成員變量的時候,

  1. 可以保證該成員變量在不同線程之間的可見性;
  2. 可以防止編譯器和處理器對該成員變量進行重新排序,保證有序性;
  3. 無法保證該成員變量的原子性,并發(fā)場景下線程不安全。

1 可見性

我們用兩個線程來模擬一下,不同線程的工作內存之間的可見性。

public class VolatileDemo {
    public static int INIT = 0;
    public static int MAX = 6;

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        // 線程1
        pool.execute(() -> {
            int v = INIT;
            while (v < MAX) {
                if (v != INIT) {
                    System.out.println(Thread.currentThread().getName() + " >>> 獲取更新后的值:" + INIT);
                    v = INIT;
                }
            }
        });
        // 線程2
        pool.execute(() -> {
            int v = INIT;
            while (INIT < MAX) {
                System.out.println(Thread.currentThread().getName() + " >>> 將值更新為:" + ++v);
                INIT = v;
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

先簡單說明一下,線程1始終運行,檢查自身工作內存中INIT的值是否有變化。線程2會修改自身工作內存中的INIT值,并同步到主內存中。所以,程序的輸出應該是:

pool-1-thread-2 >>> 將值更新為:1
pool-1-thread-1 >>> 獲取更新后的值:1
pool-1-thread-2 >>> 將值更新為:2
pool-1-thread-2 >>> 將值更新為:3
pool-1-thread-2 >>> 將值更新為:4
pool-1-thread-2 >>> 將值更新為:5
pool-1-thread-2 >>> 將值更新為:6

所以,線程2的工作內存中的INIT對于線程1來說是不可見的,線程1無法感知到線程2對于INIT的修改。

如果對INIT使用volatile關鍵字,那么任何線程修改了INIT,都要立即將他寫回到主內存中,并且這會使其他線程中的INIT數(shù)據(jù)失效,想要繼續(xù)使用他的時候,都必須要從主內存中重新獲取。

INIT加上volatile關鍵字后并運行輸出

public volatile static int INIT = 0;
pool-1-thread-2 >>> 將值更新為:1
pool-1-thread-1 >>> 獲取更新后的值:1
pool-1-thread-2 >>> 將值更新為:2
pool-1-thread-1 >>> 獲取更新后的值:2
pool-1-thread-2 >>> 將值更新為:3
pool-1-thread-1 >>> 獲取更新后的值:3
pool-1-thread-2 >>> 將值更新為:4
pool-1-thread-1 >>> 獲取更新后的值:4
pool-1-thread-2 >>> 將值更新為:5
pool-1-thread-1 >>> 獲取更新后的值:5
pool-1-thread-2 >>> 將值更新為:6
pool-1-thread-1 >>> 獲取更新后的值:6

可以看到,線程1時刻都感知到了INIT值的變化。

注意

有一種說法是volatile修飾對象或數(shù)組的時候,針對的是引用,數(shù)組或對象中的成員變量不具備可見性。

我在做這種測試的時候并沒有產生這種結果,我在做如下示例的時候volatile依然對對象中的成員變量產生影響了。不知道是我的代碼有問題還是這種說法是錯誤的。

public class VolatileDemo {
    public static InitObj initObj = new InitObj(0);
    public static int MAX = 6;

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        // 線程1
        pool.execute(() -> {
            int v = initObj.getInit();
            while (v < MAX) {
                if (v != initObj.getInit()) {
                    System.out.println(Thread.currentThread().getName() + " >>> 獲取更新后的值:" + initObj.getInit());
                    v = initObj.getInit();
                }
            }
        });
        // 線程2
        pool.execute(() -> {
            int v = initObj.getInit();
            while (initObj.getInit() < MAX) {
                System.out.println(Thread.currentThread().getName() + " >>> 將值更新為:" + ++v);
                initObj.setInit(v);
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Data
    static
    class InitObj {
        private int init;

        public InitObj(int i) {
            this.init = i;
        }
    }
}

2 有序性

驗證有序性有個很好的例子是單例模式:

public class LazySingleton {
    private static volatile LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

上面是雙重檢查鎖機制的單例模式,我們都知道synchronized關鍵字可以保證有序性,那么為什么還要加上volatile關鍵字呢?

原因就是,兩者保證有序性的方式不一樣。synchronized無法禁止指令重排,被synchronized包裹的代碼塊就算發(fā)生指令重排,由于同一時間內只有一個線程執(zhí)行邏輯,所以就算是指令重排也可以保證有序性。

而volatile則是使用內存屏障的方式禁止指令重排,從而保證有序性。內存屏障是個很底層的概念大概的作用就是重排序時不能把后面的指令重排序到內存屏障之前。

我們看一下上述代碼的部分字節(jié)碼

Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
         3: ifnonnull     37
         6: ldc           #3                  // class com/spheign/szjx/designModel/singleton/LazySingleton
         8: dup
         9: astore_0
        10: monitorenter
        11: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        14: ifnonnull     27
        17: new           #3                  // class com/spheign/szjx/designModel/singleton/LazySingleton
        20: dup
        21: invokespecial #4                  // Method "<init>":()V
        24: putstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        27: aload_0
        28: monitorexit
        29: goto          37
        32: astore_1
        33: aload_0
        34: monitorexit
        35: aload_1
        36: athrow
        37: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        40: areturn

其中17、20、21、24為instance = new LazySingleton();的主要操作。我們也可以拆分成下面三個步驟:

  1. 分配存儲LazySingleton對象的內存空間;
  2. 初始化LazySingleton對象;
  3. 將instance指向剛剛分配的內存空間。

以上是正常的順序,但是編譯器為了優(yōu)化程序的性能,有可能的執(zhí)行順序是:

  1. 分配存儲LazySingleton對象的內存空間;
  2. 將instance指向剛剛分配的內存空間;
  3. 初始化LazySingleton對象。

這時問題就來了,線程1先進來執(zhí)行,并且已經將instance指向LazySingleton對象的內存空間,但還沒有初始化LazySingleton對象。與此同時,線程2執(zhí)行到了判斷if (instance == null),由于instance指向LazySingleton對象的內存空間,所以判斷false,直接返回instance對象。這樣線程2使用instance對象的時候就會發(fā)生空指針異常。

3 原子性

我們先來看一個例子:

public class VolatileDemo {
    public volatile int i = 0;

    public void increase() {
        i++;
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}

如果你用的IDE是idea的話,那么你會發(fā)現(xiàn)一個提示

原子性提示

這是由于i++不是一個原子性的操作,我們拆分一下就是兩步操作,先執(zhí)行i+1,再將i+1賦值給i。

所以程序的運行結果肯定不會是我們預期的1000,而是小于1000的某個值。稍加分析我們就能理解為什么是這種結果,線程1和線程2都同時獲取了i的值,比如是10,線程1執(zhí)行了+1操作,將11寫回到主內存中,線程2工作內存中的i將失效。在線程1向主內存中回寫數(shù)據(jù)之前線程2也完成了+1的操作,所以就算是工作內存中的i失效了也不會影響線程2再將11寫回到主內存中。

解決辦法有兩個,加鎖或者使用atomic類。

synchroinzed:

下例中也可以不加volatile關鍵字

public class VolatileDemo {
    public volatile int i = 0;
    private final Object lock = new Object();
    public void increase() {
        synchronized (lock){
            i++;
        }

    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}

AtomicInteger:

public class VolatileDemo {
    public AtomicInteger i = new AtomicInteger(0);
    public void increase() {
            i.incrementAndGet();
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容