Java併發16:volatile關鍵字的兩種用法-一次性狀態標誌、雙重檢查單例模式
阿新 • • 發佈:2019-02-05
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(); 這個操作不是原子性的。
這個操作可以劃分為:
- 在Heap中開闢地址,進行物件初始化:new DoubleCheckSingleton()
- 將Heap中初始化完成的DoubleCheckSingleton物件地址,指向Thread Stack中的物件引用instance。
這兩步操作經過指令重拍之後可能是2->1的順序,因為在單執行緒中1->2和2->1的執行結果時一樣的。
這種指令重拍在單執行緒下毫無問題,但是在多執行緒下可能存在問題:
- 執行緒A在getInstance()方法中的執行順序是:0->2->1->3,且當前執行到了第2步,這是instance已經 ! = null 了。
- 執行緒B進入到getInstance()方法中,在第0處檢查發現instance ! = null ,所以直接執行第4步:返回instance物件。
- 執行緒B繼續進行後續操作,例如執行
instance.getName()
等操作。 - 而這時執行緒A還在執行第1步的初始化工作,這時,instance應用執行的實際地址還是null值。
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;
}
}