單例終極分析(一)

單例的用處

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

version 1: 餓漢式

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

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堆裏了,是否是有點不那麼含蓄?程序員

因而你們快速迭代出懶漢式單例spring

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解決有序性

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

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修飾後,重排只能分別發生在一、2之間或語句四、5之間。即語句一、2不能跨過語句3,語句四、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()只能按步驟一、二、3順序執行,問題就此解決。

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

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

class Singleton {

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

    public static Singleton getInstance(){
        Singleton 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單例等,敬請期待!(會有人期待嗎 ::>_<:: )

參考資料

相關文章
相關標籤/搜索