你真的會寫單例模式嗎

做者:DeppWang原文地址html

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

img

又一篇一抓一大把的博文,但是你真的的搞懂了嗎?點開看看。。過後,你也來一篇。java

單例模式是面試中很是喜歡問的了,咱們每每自認爲已經徹底理解了,沒什麼問題了。但要把它手寫出來的時候,可能出現各類小錯誤,下面是我總結的快速準確的寫出單例模式的方法。git

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

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

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

<!--more-->安全

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

類加載的時候就新建實例多線程

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 不能指向另外一個實例,這裏用不上。固然,加上也沒問題。函數

<!--// final 修飾的基本數據類型,在編譯期時,初始化數據放在常量池-->this

這個寫法有一個不足之處,就是若是須要經過參數設置實例,則沒法作到。舉個栗子:

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();

2、可經過參數設置實例的寫法

考慮到這種狀況,就在調用 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,就將新建多個實例,那單例模式就不是單例模式了。

3、改良版加鎖的寫法

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

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 修飾的變量,讀操做,必須等寫操做完成。

4、其餘非主流寫法

枚舉寫法:

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; 
    }  
}

5、小結

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

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

6、延伸閱讀


本篇文章由一文多發平臺ArtiPub自動發佈

相關文章
相關標籤/搜索