單例終極分析(一)

單例的用處

如果你看過設計模式,肯定會知道單例模式,實際上這是我能默寫出代碼的第一個設計模式,雖然很長一段時間我並不清楚單例具體是做什麼用的。
這裏簡單提一下單例的用處。作爲java程序員,你應該知道spring框架,而其中最核心的IOC,在默認情況下注入的Bean就是單例的。有什麼好處?那些Service、Dao等只創建一次,不必每次都通過new方式創建,也就不用每次都開闢空間、垃圾回收等等,會省不少資源。

version 1: 餓漢式

那麼如何寫一個單例呢?我想很多朋友都能搞定:

public class Singleton {

    private static final Singleton singletonInstance = new Singleton();    // A - 急不可待的成員變量賦值,static和final修飾
    private Singleton (){}    // B - 私有化的構造器,避免隨意new

    public static Singleton getInstance(){    // C - 暴露給外部的獲取方法
        return singletonInstance;
    }
}

Ok,擁有A、B、C三大特點(註釋部分),就構成了著名的餓漢式單例。好處在於簡單粗暴,易於理解(只要你真正通曉finalstatic的作用)。
但有豪放派,就有婉約派。後來大家都覺得,我還沒有使用這個類,你就直接把對象構建出來扔java堆裏了,是不是有點不那麼含蓄?

於是大家快速迭代出懶漢式單例

version 2: 懶漢式

class Singleton {

    private static Singleton singletonInstance;     // A - 溫婉到只有變量聲明
    private Singleton (){}      // B 

    public static Singleton getInstance(){      // C 
        if(singletonInstance==null){
            singletonInstance = new Singleton();    // D - 成員變量的創建賦值延後至此
        }
        return singletonInstance;
    }
}

變化發生於A、D兩步,總得來說,就是把成員變量singletonInstance的創建和賦值延後了。基本的要求達到了,在沒調用getInstance()方法之前,對象無創建,不再麻煩java堆大大。一切看起來都很美好,但僅限於單線程情況下
好,看看大家喜聞樂見的併發場景下,這種簡易的寫法會出現什麼問題——兩個線程T-1T-2同時訪問getInstance(),它們都覺得singletonInstance==null判斷成立,分別執行了步驟D,成功創建出singletonInstance對象!但是,我們通篇都在聊單例啊,T-1T-2的玩法無疑很不單例!
問題分析出來了,而解決上並不複雜——讓線程同步就好

version 2.1: 簡易解決併發的懶漢式

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static synchronized Singleton getInstance(){      // C - 用synchronized關鍵字修飾
        if(singletonInstance==null){
            singletonInstance = new Singleton();    // D
        }
        return singletonInstance;
    }
}

唯一的變化在於步驟C,加入了synchronized關鍵字,讓線程同步執行此方法。現在問題解決了,不管線程T-1還是T-2,在getInstance()面前都要小朋友們排排坐——一個個執行,這樣即使是線程T-100甚至T-500過來也要排隊執行,哈哈哈哈哈哈……嗚嗚嗚……
既是解決方案,也是問題所在,這種方式效率太差了

我們知道,synchronized有另一種使用方式就是鎖代碼塊,可以減少鎖粒度。

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        synchronized (Singleton.class){    // C - 改成synchronized鎖代碼塊
            if(singletonInstance==null){
                singletonInstance = new Singleton();
            }
        }
        return singletonInstance;
    }
}

但在這個例子中,該方式看上去似乎沒什麼提升(該方法主要邏輯只有singletonInstance = new Singleton()一行)。好在有聰明人,研究出了Double-check

version 2.2: Double-check (有問題版)

class Singleton {

    private static Singleton singletonInstance;     // A
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        if(singletonInstance==null){    // C1 - synchronized之前,第一次判斷
            synchronized (Singleton.class){    
                if(singletonInstance==null){    // C2 - synchronized之後,第二次判斷
                    singletonInstance = new Singleton();
                }
            }
        }
        return singletonInstance;
    }
}

我一直覺得這種方式很巧妙。C1的判斷用於非併發環境,阻攔對象創建後的大部分訪問;C2的判斷,解決首次創建對象時的併發問題。
很長一段時間,我覺得這就是最終方案了,世界再次變得美好,沒想到還是圖樣圖森破(too young, too simple!)。其實不止是單例,jdk1.5之前很多問題都被一個關鍵字耽擱了——volatile,而它相關的問題深深隱藏在Java內存模型層面,且聽我緩緩道來……

version 2.3: volatile解決有序性

算了,照顧下沒耐性的開發兄弟,先給出修改方案:

class Singleton {

    private static volatile Singleton singletonInstance;     // A - 用volatile修飾
    private Singleton (){}      // B

    public static Singleton getInstance(){      
        if(singletonInstance==null){    // C1
            synchronized (Singleton.class){    
                if(singletonInstance==null){    // C2
                    singletonInstance = new Singleton();
                }
            }
        }
        return singletonInstance;
    }
}

可以看到,唯一的變化在於A位置加入了volatile關鍵字,用於解決有序性問題。volatile涉及的原子性可見性這裏不作討論)

有序性

什麼是有序性?舉個“栗子”:

int x=2;//語句1
int y=0;//語句2
boolean flag=true;//語句3
x=4;//語句4
y=-1;//語句5

對於上面的代碼來說,書寫語句按順序1至5,但執行上很可能不是這樣。有可能是1-4-3-2-5,或者1-3-2-5-4,其實只要保證1在4前並且2在5前,剩下的順序可以隨意變化。這要感謝內存模型同志,它天然允許編譯器和處理器對指令進行重排序。動機是好的——可以默默的幫你做些優化,但在併發場景下,就有好心辦壞事的嫌疑。

看下另一個例子:

Context context = null;
boolean inited = false;

   //線程-1:
public void methodA(){
    context=loadContext();    //語句1
    inited=true;    //語句2
}

    //線程-2:
public void methodB(){
    while(!inited){
        sleep(1)    //語句3
    }
    doSomethingwithconfig(context);    //語句4
}

併發場景下,很可能出現如下情況:

clipboard.png

  • 線程-2語句3位置無憂無慮的休眠
  • 語句2語句1發生指令重排,線程-1進入methodA()時先執行語句2
  • 恰逢線程-2覺醒,執行語句4,此時context還是null(語句1context初始化還沒執行),災難產生

volatile,是個“擋板”,能保證執行順序。爲什麼稱之爲“擋板”?還以之前的“栗子”說明:

int x=2;//語句1
int y=0;//語句2
volatile boolean flag=true;    //語句3 - 用volatile修飾
x=4;//語句4
y=-1;//語句5

語句3boolean變量 用volatile修飾後,重排只能分別發生在1、2之間或語句4、5之間。即語句1、2不能跨過語句3,語句4、5也不能跨過語句3

我們還需知道,對於java的某些操作,比如++,雖然看上去是一行代碼,但實質上這個操作本身並不是原子的。以i++爲例,該操作實際包含i的當前值獲取,i+1計算,以及i=的賦值操作三兄弟。

同樣的,singletonInstance = new Singleton()也非原子指令,包含:

  1. 對象內存分配
  2. 初始化LazySingleton對象屬性
  3. 將singleton引用指向內存空間

如果不用volatile修飾,萬惡的指令重排可能發生在步驟2步驟3之間,產生如下狀況(此處有盜圖嫌疑,罪過):

clipboard.png

以上圖的情況,線程B獲取到了尚未初始化完全的LazySingleton對象,使得在後續的使用中出現異常! 用volatile修飾singleton變量後,指令重排技能被禁用,singletonInstance = new Singleton()只能按步驟1、2、3順序執行,問題就此解決。

值得一提的是,其實存在更好的volatile修飾版本。

version 2.4:推薦的volatile + Double-check 版

class Singleton {

    private static volatile Singleton singletonInstance;     // A 
    private Singleton (){}      // B

    public static Singleton getInstance(){
        tempInstance = singletonInstance;    // C - 開啓了臨時變量
        if(tempInstance==null){    
            synchronized (Singleton.class){    
                if(tempInstance==null){
                    singletonInstance = tempInstance = new Singleton();
                }
            }
        }
        return tempInstance ;
    }
}

這種寫法差別在於在代碼C位置,聲明瞭變量tempInstance臨時變量,之後的邏輯都使用tempInstance代替singletonInstance。爲什麼要這樣做?wiki上準原文是這麼說的:

Note the local variable "tempInstance ", which seems unnecessary. The effect of this is that in cases where singletonInstance is already initialized (i.e., most of the time), the volatile field is only accessed once (due to "return tempInstance ;" instead of "return singletonInstance;"), which can improve the method's overall performance by as much as 25 percent.

翻譯一下就是:
singletonInstance對象大部分時候是已完成初始化的,用tempInstance臨時變量之後能減少volatile屬性(singletonInstance)的訪問,這麼做大概能提升25%的性能!

後續

哇,一不小心寫了這麼多,而且還沒結束,留待下一篇吧。(主要是volatile部分比較羅嗦了,這個關鍵字各位需好好看下,藉以窺探內存模型,原子性和可見性沒做分析都已經佔了這麼大的篇幅)
下一篇文章會包含靜態內部類實現單例final+泛型實現單例java9 VarHandler單例等,敬請期待!(會有人期待嗎 ::>_<:: )

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章