[轉]如何正確地寫出單例模式

轉自如何正確地寫出單例模式
另外可參考
你真的會寫單例模式嗎
Java設計模式—單例設計模式(Singleton Pattern)完全解析

單例模式算是設計模式中最容易理解,也是最容易手寫代碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主要對幾種單例寫法的整理,并分析其優缺點。很多都是一些老生常談的問題,但如果你不知道如何創建一個線程安全的單例,不知道什么是雙檢鎖,那這篇文章可能會幫助到你。

懶漢式,線程不安全
當被問到要實現一個單例模式時,很多人的第一反應是寫出如下的代碼,包括教科書上也是這樣教我們的。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
          instance = new Singleton();
       }
       return instance;
    }
}

這段代碼簡單明了,而且使用了懶加載模式,但是卻存在致命的問題。當有多個線程并行調用 getInstance() 的時候,就會創建多個實例。也就是說在多線程下不能正常工作。

懶漢式,線程安全
為了解決上面的問題,最簡單的方法是將整個 getInstance() 方法設為同步(synchronized)。

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

雖然做到了線程安全,并且解決了多實例的問題,但是它并不高效。因為在任何時候只能有一個線程調用 getInstance() 方法。但是同步操作只需要在第一次調用時才被需要,即第一次創建單例實例對象時。這就引出了雙重檢驗鎖。

雙重檢驗鎖
雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。為什么在同步塊內還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了。

public static Singleton getSingleton() {
    if (instance == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (instance == null) {                 //Double Checked
                instance = new Singleton();
            }
        }
    }
    return instance ;
}

這段代碼看起來很完美,很可惜,它是有問題。主要在于instance = new Singleton()這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

  • 給 instance 分配內存
  • 調用 Singleton 的構造函數來初始化成員變量
  • 將instance對象指向分配的內存空間(執行完這步 instance 就為非 null 了)
    但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后順理成章地報錯。

我們只需要將 instance 變量聲明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton instance; //聲明成 volatile
    private Singleton (){}

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

有些人認為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操作后面會有一個內存屏障(生成的匯編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之后或者 1-3-2 之后,不存在執行到 1-3 然后取到值的情況。從「先行發生原則」的角度理解的話,就是對于一個 volatile 變量的寫操作都先行發生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。

但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內存模型)是存在缺陷的,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之后才可以放心使用 volatile。

相信你不會喜歡這種復雜又隱含問題的方式,當然我們有更好的實現線程安全的單例模式的辦法。

** 餓漢式 static final field**
這種方法非常簡單,因為單例的實例被聲明成 static 和 final 變量了,在第一次加載類到內存中時就會初始化,所以創建實例本身是線程安全的。

public class Singleton{
    //類加載時就初始化
    private static final Singleton instance = new Singleton();
    
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}

這種寫法如果完美的話,就沒必要在啰嗦那么多雙檢鎖的問題了。缺點是它不是一種懶加載模式(lazy initialization),單例會在加載類后一開始就被初始化,即使客戶端沒有調用 getInstance()方法。餓漢式的創建方式在一些場景中將無法使用:譬如 Singleton 實例的創建是依賴參數或者配置文件的,在 getInstance() 之前必須調用某個方法設置參數給它,那樣這種單例寫法就無法使用了。

靜態內部類 static nested class
我比較傾向于使用靜態內部類的方法,這種方法也是《Effective Java》上所推薦的。

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE; 
    }  
}

這種寫法仍然使用JVM本身機制保證了線程安全問題;由于 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。

枚舉 Enum
用枚舉寫單例實在太簡單了!這也是它最大的優點。下面這段代碼就是聲明枚舉實例的通常做法。

public enum EasySingleton{
    INSTANCE;
}

我們可以通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新創建新的對象。但是還是很少看到有人這樣寫,可能是因為不太熟悉吧。

總結

一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。上述所說都是線程安全的實現,文章開頭給出的第一種方法不算正確的寫法。

就我個人而言,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載(lazy initialization)會傾向于使用靜態內部類,如果涉及到反序列化創建對象時會試著使用枚舉的方式來實現單例。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • 1 場景問題# 1.1 讀取配置文件的內容## 考慮這樣一個應用,讀取配置文件的內容。 很多應用項目,都有與應用相...
    七寸知架構閱讀 6,865評論 12 68
  • 單例模式(SingletonPattern)一般被認為是最簡單、最易理解的設計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,284評論 4 34
  • 單例模式算是設計模式中最容易理解,也是最容易手寫代碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主...
    Jason_Msbaby閱讀 237評論 0 0
  • 單例模式算是設計模式中最容易理解,也是最容易手寫代碼的模式了吧。但是其中的坑卻不少,所以也常作為面試題來考。本文主...
    神奇的藍胖子閱讀 295評論 0 0
  • 從MVC說起 任何模式的發明,都是由于人們疲倦與對相同過程的重復思考。大抵類似于,你愿意經歷一次計算324x432...
    前端夫婦閱讀 276評論 0 1