你真的會寫單例模式嗎

作者:DeppWang、原文地址

人生在世,誰不面試。單例模式:一個搞懂不加分,不搞懂減分的知識點

img

又一篇一抓一大把的博文,可是你真的的搞懂了嗎?點開看看。。事后,你也來一篇。

單例模式是面試中非常喜歡問的了,我們往往自認為已經完全理解了,沒什么問題了。但要把它手寫出來的時候,可能出現各種小錯誤,下面是我總結的快速準確的寫出單例模式的方法。

單例模式有各種寫法,什么「雙重檢鎖法」、什么「餓漢式」、什么「飽漢式」,總是記不住、分不清。這就對了,人的記憶力是有限的,我們應該記的是最基本的單例模式怎么寫。

單例模式:一個類有且只能有一個對象(實例)。單例模式的 3 個要點:

  1. 外部不能通過 new 關鍵字(構造函數)的方式新建實例,所以構造函數為私有:private Singleton(){}
  2. 只能通過類方法獲取實例,所以獲取實例的方法為公有、且為靜態:public static Singleton getInstance()
  3. 實例只能有一個,那只能作為類變量的「數據」,類變量為靜態 (另一種記憶:靜態方法只能使用靜態變量):private static Singleton instance

一、最基礎、最簡單的寫法

類加載的時候就新建實例

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
    
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}

// Here is how to access this Singleton class
Singleton.getInstance().show();

當執行 Singleton.getInstance() 時,類加載器加載 Singleton.class 進虛擬機,虛擬機在方法區(元數據區)為類變量分配一塊內存,并賦值為空。再執行 <client>() 方法,新建實例指向類變量 instance。這個過程在類加載階段執行,并由虛擬機保證線程安全。所以執行 getInstance() 前,實例就已經存在,所以 getInstance() 是線程安全的。

很多博文說 instance 還需要聲明為 final,其實不用。final 的作用在于不可變,使引用 instance 不能指向另一個實例,這里用不上。當然,加上也沒問題。

這個寫法有一個不足之處,就是如果需要通過參數設置實例,則無法做到。舉個栗子:

class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    // 不能設置 name!
    public static Singleton getInstance(String name) {
        return instance;
    }
    
    public void show(){
        System.out.println("Singleon using static initialization in Java");
    }
}

// Here is how to access this Singleton class
Singleton.getInstance(String name).show();

二、可通過參數設置實例的寫法

考慮到這種情況,就在調用 getInstance() 方法時,再新建實例。

public class Singleton {
    private static Singleton instance;

    private String name;

    private Singleton(String name) {
        this.name = name;
    }

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

    public String show() {
        return name;
    }
}

Singleton.getInstance(String name).show();

這里加了 synchronized 關鍵字,能保證只會生成一個實例,但效率不高。因為實例創建成功后,再獲取實例時就不用加鎖了。

當不加 synchronized 時,會發生什么:

instance 是類的變量,類存放在方法區(元數據區),元數據區線程共享,所以類變量 instance 線程共享,類變量也是在主內存中。線程執行 getInstance() 時,在自己工作內存新建一個棧幀,將主內存的 instance 拷貝到工作內存。多個線程并發訪問時,都認為 instance == null,就將新建多個實例,那單例模式就不是單例模式了。

三、改良版加鎖的寫法

實現只在創建的時候加鎖,獲取時不加鎖。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

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

為什么要判斷兩次:

多個線程將 instance 拷貝進工作內存,即多個線程讀取到 instance == null,雖然每次只有一個線程進入 synchronized 方法,當進入線程成功新建了實例,synchronized 保證了可見性(在 unlock 操作前將變量寫回了主內存),此時 instance 不等于 null 了,但其他線程已經執行到 synchronized 這里了,某個線程就又會進入 synchronized 方法,如果不判斷一次,又會再次新建一個實例。

為什么要用 volatile 修飾 instance:

synchronized 可以實現原子性、可見性、有序性。其中實現原子性:一次只有一個線程執行同步塊的代碼。但計算機為了提升運行效率,會指令重排序。

代碼 instance = new Singleton(); 會被拆為 3 步執行。

  • A:分配一塊內存空間
  • B:在內存空間位置新建一個實例
  • C:將引用指向實例,即,引用存放實例的內存空間地址

如果 instance 都在 synchronized 里面,那么沒啥問題,問題出現在 instance 在 synchronized 外邊,因為此時外邊一群餓狼(線程),就在等待一個 instance 這塊肉不為 null。

模擬一下指令重排序的出錯場景:多線程環境下,正好一個線程,在同步塊中按 ACB 執行,執行到 AC 時(并將 instance 寫回了主內存),另一個線程執行第一個判斷時,認為 instance 不為空,返回 instance,但此時 instance 還沒被正確初始化,所以出錯。

當 instance 被 volatile 修飾時,只有 ACB 執行完了之后,其他線程才能讀取 instance

為什么 volatile 能禁止指令重排序:它在 ACB 后添加一個 lock 指令,lock 指令之前的操作執行完成后,后面的操作才能執行

你可能認為上面的解釋太復雜,不好理解。對,確實比較復雜,我也搞了很久才搞明白。你可以看看這個是不是更好理解,Java 虛擬機規范的其中一條先行發生原則:對 volatile 修飾的變量,讀操作,必須等寫操作完成。

四、其他非主流寫法

枚舉寫法:

public enum EasySingleton{
    INSTANCE;
}

當面試官讓我寫一個單例模式,我總是覺得寫這個好像有點另類

靜態內部類寫法:

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

五、小結

單例模式主要為了節省內存開銷,Spring 容器的 Bean 就是通過單例模式創建出來的。

單例模式沒寫出來,那也沒啥事,因為那下一個問題你也不一定能答出來 :)。

六、延伸閱讀

本文由博客一文多發平臺 OpenWrite 發布!

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

推薦閱讀更多精彩內容