單例模式(下) – 聊一聊單例模式的幾種寫法

帥地發表於2018-12-30

在上一篇文章 單例模式(上)—如何優雅地保證執行緒安全問題中,我們採取了懶漢式寫法來寫我們的單例模式,並且重點講解了懶漢式中執行緒安全的問題。這篇我們來講講單例模式中的其他幾種寫法。

上篇文章中,方法和變數的宣告都忘了加上“static”的宣告,這裡提醒一下。

懶漢式

懶漢式在上節我們已經講過了,直接給出程式碼:

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

複製程式碼

懶漢式這種方式需要我們來自己加鎖,保證執行緒安全的問題。

不過就算我們保證了執行緒安全,這種寫法還是無法保證存在唯一一個物件例項。因為別人還是可以通過反射的方式來建立一個新的物件。我寫個示例:

public class Singleton {
    public static void main(String[] args) throws Exception{
        //獲得構造器
        Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
        //把構造器設定為可訪問
        c.setAccessible(true);
        //建立兩個例項物件
        Singleton s1 = c.newInstance();
        Singleton s2 = c.newInstance();
        //比較下兩個例項是否相等
        System.out.println(s1 == s2);
    }
}

複製程式碼

列印結果:false。

所以懶漢式這種方式還是存在一些缺點的。

餓漢式

所謂餓漢式,就是一開始把物件例項建立出來,而不是等getInstance這個方法被呼叫才來建立物件。程式碼如下:

public class Singleton2 {
    private static Singleton2 instance = new Singleton2();
    //私有構造器
    private Singleton2(){};

    public static Singleton2 getInstance() {
        return instance;
    }
}
複製程式碼

餓漢式與懶漢式相比,我們不用管執行緒安全的問題,程式碼看起來也比較簡潔。

但是,由於物件一開始就被建立出來了,假如我們從頭到尾都不呼叫getInstance()這個方法,那麼這個物件就白建立了。

當然,和懶漢式一樣,餓漢式也存在反射問題。

總結一下餓漢式的一些問題:

1、有可能出現物件白白浪費的情況。

2、和懶漢式一樣,無法組織反射問題。

採用靜態內部類的寫法

直接上程式碼

public class Singleton3 {
    //靜態內部類
    private static class LazyHolder{
        private static Singleton3 instance = new Singleton3(); 
    }
    //私有構造器
    private Singleton3(){};
    public static Singleton3 getInstance() {
        return LazyHolder.instance;
    }
}
複製程式碼

由於外部類無法訪問靜態內部類,因此只有當外部類呼叫Singleton.getInstance()方法的時候,才能得到instance例項。

並且,instance例項物件初始化的時機並不是在Singleton被載入的時候,而是當getInstance()方法被呼叫的時候,靜態內部類才會被載入,這時instance物件才會被初始化。並且也是執行緒安全的。

所以,與餓漢式相比,通過靜態內部類的方式,可以保證instance例項物件不會被白白浪費。

但是,它仍然存在反射問題。

採取列舉的方式

直接上程式碼:

public enum Singleton4 {
    //一般用大寫的了,不過為了和前面的統一
    //我就用小寫的了
    
    instance;
}
複製程式碼

列舉的方式簡單吧?一行程式碼就搞定了,不過和餓漢式一樣,由於一開始instance例項就被建立了,所以有可能出現白白浪費的情況。

但是,通過列舉的方式,不僅程式碼簡單,執行緒安全,而且JVM還能阻止反射獲取列舉類的私有構造器。

下面做個實驗

public enum Singleton4 {
    //一般用大寫的了,不過為了和前面的統一
    //我就用小寫的了
    instance;

    public static void main(String[] args) throws Exception{
        //獲得構造器
        Constructor<Singleton4> c = Singleton4.class.getDeclaredConstructor();
        //把構造器設定為可訪問
        c.setAccessible(true);
        //建立兩個例項物件
        Singleton4 s1 = c.newInstance();
        Singleton4 s2 = c.newInstance();
        //比較下兩個例項是否相等
        System.out.println(s1 == s2);
    }
}
複製程式碼

結果出現了異常:

Exception in thread “main” java.lang.NoSuchMethodException: singleton.Singleton4.()

at java.lang.Class.getConstructor0(Class.java:3082)

at java.lang.Class.getDeclaredConstructor(Class.java:2178)

at singleton.Singleton4.main(Singleton4.java:12)
複製程式碼

所以,這種列舉的方式可以說的用的最多的一種方式了,唯一的缺點就是物件一開始就被建立,可能出現白白浪費沒有用到物件的情況。

不過,總體上,還是推薦採用列舉的方式來寫。

獲取更多原創文章,可以關注下我的公眾號:苦逼的碼農,我會不定期分享一些資源和軟體等。同時也感謝把文章介紹給更多需要的人。

相關文章