1. 程式人生 > >Java併發16:volatile關鍵字的兩種用法-一次性狀態標誌、雙重檢查單例模式

Java併發16:volatile關鍵字的兩種用法-一次性狀態標誌、雙重檢查單例模式

volatile關鍵字在之前的章節中多次提及:

本章主要就volatile關鍵字的兩種實際用法進行說明。

1.volatile概述

  • volatile,即易變的,在Java中標識一個變數是易變變數
  • volatile可以看成輕量級的synchronized,相對於synchronized:編碼簡單、資源開銷較少,同樣的實現的功能也有限。
  • volatile能夠保證變數的可見性,但是並不能保證變數的原子性和有序性。
  • 使用volatile的前提,不受原子性和有序性影響:變數狀態完全獨立於任何程式的其他狀態。

2.volatile的兩種用法

本文只對volatile常見的兩種用法進行學習:

  • 一次性狀態標誌
  • 一種單例模式:雙重檢查單例模式

2.1.一次性狀態標誌

應用場景:使用一個布林變數來標記狀態,用於指示發生了一件重要的一次性事件。例如:標誌配置完成了初始化、標誌某種服務關閉了服務等等。

編碼場景:

  • 一臺咖啡機能夠不停地服務:為客人制作咖啡。
  • 這臺咖啡機有一個關閉按鈕,當按下關閉按鈕是,咖啡機停止服務。

2.1.1.不使用volatile關鍵字

狀態標誌位:

/**
 * 是否關閉
 */
private static boolean shutdown = false;

咖啡機:

/**
 * <p>Title: 咖啡機</p>
 *
 * @author
韓超 2018/3/16 13:58 */
static class CoffeeMaker { /** * 關閉關閉咖啡機 */ public static void shutdown() { shutdown = true; System.out.println("關閉了咖啡機..."); } /** * 生成開發 */ public static void makeCoffee(String name) { System.out.println("咖啡機開始為客戶製作咖啡..."
); while (!shutdown) ; System.out.println("咖啡機已經停止工作,不再對外提供服務!"); } }

製作咖啡與關閉咖啡機:

//開始製作咖啡
new Thread(() -> {
    CoffeeMaker.makeCoffee(Thread.currentThread().getName());
}).start();
Thread.sleep(100);
//關掉咖啡機
new Thread(() -> {
    CoffeeMaker.shutdown();
}).start();

執行結果:

咖啡機開始為客戶製作咖啡...
關閉了咖啡機...

結果分析:

  • 雖然執行緒第二個執行緒將是否關閉這個狀態為置為了true,但是第一個執行緒並沒有覺察到這種狀態變化,導致咖啡機無法停止工作。
  • 這是由於不採取任務措施的情況下,共享變數的修改對其他執行緒未必是可見的。

2.1.2.使用volatile關鍵字

將上面的程式碼做一個很小的修改:將是否關閉這個狀態用volatile關鍵字標記:

/**
 * 是否關閉
 */
 private volatile static boolean shutdown = false;

然後再次執行測試程式碼,執行結果如下:

咖啡機開始為客戶製作咖啡...
關閉了咖啡機...
咖啡機已經停止工作,不再對外提供服務!

2.1.3.關於一次性狀態標誌用法的分析

使用一次性狀態標誌的關鍵點:

  • 1.狀態標誌的狀態轉換是原子操作。例如上面的程式碼中,對布林型別進行賦值操作,在Java中是原子性操作。
  • 2.只有一次性的狀態轉換。上面的程式碼中,狀態標誌位只是從false轉換為true,並沒有繼續進行從true到false的轉換等。這種轉換的一次性杜絕了有序性問題的產生。

可以看到,上述兩個關鍵點,其實就是對原子性和有序性的保證。

也就是說,如果要是用volatile進行併發程式設計,就需要通過其他手段來保證程式碼的原子性和有序性。

2.2.雙重檢查

雙重檢查是一種單例模式的實現方式,關於單例模式這裡不多做介紹。

2.2.1.不加volatile的雙重檢查模式

/**
 * <p>雙重檢測單例模式--不加volatile關鍵字</p>
 *
 * @author hanchao 2018/3/17 19:14
 **/
static class DoubleCheckSingleton {

    private static DoubleCheckSingleton instance = null;

    private DoubleCheckSingleton() {
    }

    public static synchronized DoubleCheckSingleton getInstance() {
        if (instance == null) {//0.null判斷
            synchronized (DoubleCheckSingleton.class) {
                if (instance == null) {
                    //耗時較長的初始化操作
                    instance = new DoubleCheckSingleton();//1.初始化   2.物件地址指向引用
                }
            }
        }
        return instance;//3.返回物件
    }
}

這種模式存在問題:

  • instance = new DoubleCheckSingleton(); 這個操作不是原子性的。

這個操作可以劃分為:

  1. 在Heap中開闢地址,進行物件初始化:new DoubleCheckSingleton()
  2. 將Heap中初始化完成的DoubleCheckSingleton物件地址,指向Thread Stack中的物件引用instance。

這兩步操作經過指令重拍之後可能是2->1的順序,因為在單執行緒中1->2和2->1的執行結果時一樣的。

這種指令重拍在單執行緒下毫無問題,但是在多執行緒下可能存在問題:

  1. 執行緒A在getInstance()方法中的執行順序是:0->2->1->3,且當前執行到了第2步,這是instance已經 ! = null 了。
  2. 執行緒B進入到getInstance()方法中,在第0處檢查發現instance ! = null ,所以直接執行第4步:返回instance物件。
  3. 執行緒B繼續進行後續操作,例如執行instance.getName()等操作。
  4. 而這時執行緒A還在執行第1步的初始化工作,這時,instance應用執行的實際地址還是null值。
  5. instance.getName()等操作,會報NullPointerException異常。

2.2.2.新增volatile的雙重檢查模式

java的記憶體模式在持續改進之中,在jdk5中提到:

Updates for J2SE 5.0 (aka 1.5, Tiger)
In particular, double-check idioms work in the expected way when references are declared volatile.

也就是說:在JDK1.5及以後的版本中,通過將物件引用宣告成volatile的,是可以正常使用雙重檢查模式的。

新增volatile的雙重檢查模式的程式碼如下:

/**
 * <p>雙重檢測單例模式--加volatile關鍵字</p>
 *
 * @author hanchao 2018/3/17 19:10
 **/
static class DoubleCheckedVolatileSingleton {
    //注意這裡是volatile的
    private volatile static DoubleCheckedVolatileSingleton instance = null;

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

參考文獻