單例模式相信你們都不陌生,學習設計模式的時候,每每第一個要學習的就是單例模式。單例模式在Java中有許多實現,最多見的是「雙重鎖檢測」、「靜態內部類」以及「枚舉」的實現方式。《Effective Java》推薦使用枚舉的方式。html
但今天要討論是使用「雙重鎖檢測」實現單例的時候,關於volatile關鍵字引起的一些探索和思考。限於篇幅緣由,本文假設你已經瞭解如下知識:java
一個不使用volatile的雙重鎖檢驗單例模式大概長這樣:設計模式
public class Singleton {
private static Singleton instance; // 不使用volatile關鍵字
// 雙重鎖檢驗
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
複製代碼
這個代碼會有什麼問題?咱們知道,對一個鎖的解鎖happens-before隨後對這個鎖的加鎖。粗略一看,上述代碼是沒有太大問題的。加鎖操做並不能保證同步區內的代碼不會發生重排序。對於第10行,是可能會被JVM分解和重排序的,也就是說:bash
instance = new Singleton(); // 第10行
// 能夠分解爲如下三個步驟
1 memory=allocate();// 分配內存 至關於c的malloc
2 ctorInstanc(memory) //初始化對象
3 s=memory //設置s指向剛分配的地址
// 上述三個步驟可能會被重排序爲 1-3-2,也就是:
1 memory=allocate();// 分配內存 至關於c的malloc
3 s=memory //設置s指向剛分配的地址
2 ctorInstanc(memory) //初始化對象
複製代碼
而一旦假設發生了這樣的重排序,好比線程A在第10行執行了步驟1和步驟3,可是步驟2尚未執行完。這個時候線程A執行到了第7行,它會斷定instance不爲空,而後直接返回了一個未初始化完成的instance!app
針對上述問題,在Java 5 之後,JMM模型容許咱們使用volatile關鍵字禁止這樣的重排序。對於JMM的happens-before規則,即對一個volatile修飾的變量的寫操做,happens-before隨後對這個變量的讀操做。因此咱們能夠在聲明instance的時候,給它加上volatile關鍵字。ide
public class Singleton {
private static volatile Singleton instance; // 使用volatile關鍵字
// 雙重鎖檢驗
public static Singleton getInstance() {
if (instance == null) { // 第7行
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 第10行
}
}
}
return instance;
}
}
複製代碼
OK,問題彷佛解決了。可是筆者心底仍然有一個疑問:假設沒有使用volatile,真的會返回一個未初始化完成的實例嗎?實例未初始化完成會怎樣?函數
先來看看一個Java對象實例化的過程:工具
1.先爲對象分配空間,並按屬性類型默認初始化
ps:八種基本數據類型,按照默認方式初始化,其餘數據類型默認爲null
2.父類屬性的初始化(包括代碼塊,和屬性按照代碼順序進行初始化)
3.父類構造函數初始化
4.子類屬性的初始化(同父類同樣)
5.子類構造函數的初始化學習
在好奇心的驅使下,我寫了一個Demo代碼作了一個實驗:this
// 單例代碼
public class Singleton {
private static Singleton instance; // 不加volatile
private volatile boolean flag = false; // 一個flag來標識初始化是否完成
private Singleton() {
try {
Thread.sleep(1000);
flag = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 給客戶端調用的,若是初始化未完成,應該返回false,若是完成,返回true
public boolean isFlag() {
return flag;
}
// 雙重鎖檢查實現單例模式
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複製代碼
// 客戶端代碼
public class SingletonDemo {
private final static int THREAD_NUMBER = 1000; // 線程數量
private static class MyThread implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
if (!singleton.isFlag()) {
System.out.println("I am false!!!");
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(new MyThread()).start();
}
}
}
複製代碼
若是按照上述推斷,有可能返回一個未初始化完成的實例的話,客戶端調用isFlag()
方法是有可能返回false
的。
神奇的事情發生了,我反覆調整了各類參數(線程數量和睡眠時間)並運行了屢次,發現並無打印出「I am false!!!」這句話!也就是說,那個地方沒有發生咱們理論上說的重排序!
到底是什麼緣由呢?爲何沒有發生重排序呢?
在網上找到這篇文章:The "Double-Checked Locking is Broken" Declaration,其中說到:若是使用Symantec JIT(一個基於句柄方式訪問對象的編譯器),它編譯出來的代碼就會發生上述的重排序。
筆者沒有可以找到Symantec JIT或一個其它的基於句柄方式訪問對象的編譯器來實驗。不過看了一下HotSpot的反編譯結果。
咱們用HotSpot的javap工具來反編譯一下:
javac Singleton.java
javap -l -v Singleton.class
複製代碼
public static communication.Singleton getInstance();
descriptor: ()Lcommunication/Singleton;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: getstatic #8 // Field instance:Lcommunication/Singleton;
3: ifnonnull 37
6: ldc #9 // class communication/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #8 // Field instance:Lcommunication/Singleton;
14: ifnonnull 27
17: new #9 // class communication/Singleton
20: dup
21: invokespecial #10 // Method "<init>":()V
24: putstatic #8 // Field instance:Lcommunication/Singleton;
27: aload_0
28: monitorexit
// 省略
複製代碼
從序號17到序號24應該就是new一個對象的過程。逐一解釋一下:
能夠看到,它是先進行實例化,再存入到靜態變量instance中。也就是說,這個地方沒有發生以前說的重排序。
再來看看Java訪問對象的兩種方式:使用句柄訪問和直接訪問。
再聯想到以前說的可能出現的重排序結果,咱們可能有這樣一個猜測:只有句柄訪問方式纔有可能發生那種重排序。
若是咱們使用一個基於直接訪問對象的編譯器(如HotSpot默認編譯器),這個地方不加volatile關鍵字也不會出現問題。
而若是咱們使用一個基於句柄方式訪問對象的編譯器(如Symantec JIT),不加volatile關鍵字可能會致使重排序,返回一個未初始化完成的實例。
此結論並不保證必定正確,只是基於目前現有的信息進行的猜測,若是要證明,可能還須要進一步實驗。若是您有嚴瑾的理論或更詳盡的實驗數據,歡迎聯繫筆者。