單例模式的幾種實現

單例模式
     作用:確保一個類只有一個實例,並提供該實例的全局訪問點
     結構:使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。

               私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。

     實現:

(一)懶漢式(線程不安全)

以下實現中,私有靜態變量 uniqueInstance 被延遲化實例化,這樣做的好處是,如果沒有用到該類,那麼就不會實例化 uniqueInstance,從而節約資源。

這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入 if (uniqueInstance == null) ,並且此時 uniqueInstance 爲 null,那麼多個線程會執行 uniqueInstance = new Singleton(); 語句,這將導致多次實例化 uniqueInstance。

//單例模式   懶漢式(線程不安全)
public class Singleton1 {
    private static Singleton1 uniqueInstance;
    private Singleton1(){}
    public static Singleton1 getUniqueInstance(){
        if(uniqueInstance==null){
            uniqueInstance=new Singleton1();
        }
        return uniqueInstance;
    }
}

測試:

public class Test {
    
    @org.junit.Test
    public void test1() {
        Singleton1 singleton1=Singleton1.getUniqueInstance();
        Singleton1 singleton2=Singleton1.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

cn.edu.ccit.singleton.Singleton1@514713
cn.edu.ccit.singleton.Singleton1@514713

(二)懶漢式(線程安全)

只需要對 getUniqueInstance() 方法加鎖,那麼在一個時間點只能有一個線程能夠進入該方法,從而避免了對 uniqueInstance 進行多次實例化的問題。

但是這樣有一個問題,就是當一個線程進入該方法之後,其它線程試圖進入該方法都必須等待,因此性能上有一定的損耗。

//單例模式   懶漢式(線程安全)
public class Singleton2 {
    private static Singleton2 uniqueInstance;
    private Singleton2(){}
    public static synchronized Singleton2 getUniqueInstance(){
        if(uniqueInstance==null){
            uniqueInstance=new Singleton2();
        }
        return uniqueInstance;
    }
}

測試:

public class Test {
    
    @org.junit.Test
    public void test2() {
        Singleton2 singleton1=Singleton2.getUniqueInstance();
        Singleton2 singleton2=Singleton2.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

cn.edu.ccit.singleton.Singleton2@514713
cn.edu.ccit.singleton.Singleton2@514713

(三)餓漢式(線程安全)

線程不安全問題主要是由於 uniqueInstance 被實例化了多次,如果 uniqueInstance 採用直接實例化的話,就不會被實例化多次,也就不會產生線程不安全問題。但是直接實例化的方式也丟失了延遲實例化帶來的節約資源的優勢。

//單例模式   餓漢式(線程安全)
public class Singleton3 {
    private static Singleton3 uniqueInstance=new Singleton3();
    private Singleton3(){}
    public static Singleton3 getUniqueInstance(){
        return uniqueInstance;
    }
}

測試:

public class Test {

    @org.junit.Test
    public void test3() {
        Singleton3 singleton1=Singleton3.getUniqueInstance();
        Singleton3 singleton2=Singleton3.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

cn.edu.ccit.singleton.Singleton3@514713
cn.edu.ccit.singleton.Singleton3@514713

(四)雙重校驗鎖(線程安全)

uniqueInstance 只需要被實例化一次,之後就可以直接使用了。加鎖操作只需要對實例化那部分的代碼進行。也就是說,只有當 uniqueInstance 沒有被實例化時,才需要進行加鎖。

雙重校驗鎖先判斷 uniqueInstance 是否已經被實例化,如果沒有被實例化,那麼纔對實例化語句進行加鎖。

//單例模式   雙重校驗鎖(線程安全)
public class Singleton4 {
    private volatile static Singleton4 uniqueInstance;
    private Singleton4(){}
    public static Singleton4 getUniqueInstance(){
        if(uniqueInstance==null){
            synchronized (Singleton4.class) {
                if(uniqueInstance==null){
                    uniqueInstance=new Singleton4();
                }
            }
        }
        return uniqueInstance;
    }
}

測試:

public class Test {

    @org.junit.Test
    public void test4() {
        Singleton4 singleton1=Singleton4.getUniqueInstance();
        Singleton4 singleton2=Singleton4.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

cn.edu.ccit.singleton.Singleton4@514713
cn.edu.ccit.singleton.Singleton4@514713

考慮下面的實現,也就是隻使用了一個 if 語句。在 uniqueInstance == null 的情況下,如果兩個線程同時執行 if 語句,那麼兩個線程就會同時進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個線程都會執行 uniqueInstance = new Singleton(); 這條語句,只是先後的問題,也就是說會進行兩次實例化,從而產生了兩個實例。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句。

if(uniqueInstance==null){
            synchronized (Singleton4.class) {
                 uniqueInstance=new Singleton4(); 
            }
        }

uniqueInstance 採用 volatile 關鍵字修飾也是很有必要的。uniqueInstance = new Singleton(); 這段代碼其實是分爲三步執行。

  1. 分配內存空間

  2. 初始化對象

  3. 將 uniqueInstance 指向分配的內存地址

但是由於 JVM 具有指令重排的特性,有可能執行順序變爲了 1>3>2,這在單線程情況下自然是沒有問題。但如果是多線程下,有可能獲得是一個還沒有被初始化的實例,以致於程序出錯。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。

(五)靜態內部類實現(線程安全)

當 Singleton 類加載時,靜態內部類 SingletonHolder 沒有被加載進內存。只有當調用 getUniqueInstance() 方法從而觸發SingletonHolder.INSTANCE 時 SingletonHolder 纔會被加載,此時初始化 INSTANCE 實例。

這種方式不僅具有延遲初始化的好處,而且由虛擬機提供了對線程安全的支持。

//單例模式   靜態內部類實現(線程安全)
public class Singleton5 {
    private Singleton5(){}
    private static class Singleton5Holder{
        private static final Singleton5 INSTANCE=new Singleton5();
    }
    public static Singleton5 getUniqueInstance(){
        
        return Singleton5Holder.INSTANCE;
    }
}

測試:

public class Test {
    
    @org.junit.Test
    public void test5() {
        Singleton5 singleton1=Singleton5.getUniqueInstance();
        Singleton5 singleton2=Singleton5.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

cn.edu.ccit.singleton.Singleton5@514713
cn.edu.ccit.singleton.Singleton5@514713

(六)枚舉實現(線程安全)

這是單例模式的最佳實踐,它實現簡單,並且在面對複雜的序列化或者反射×××的時候,能夠防止實例化多次。

//單例模式   枚舉實現(線程安全)
public enum Singleton6 {
    Singleton;
    private Object uniqueInstance=null;
    private Singleton6(){
        uniqueInstance=new Object();
    }
    public  Object getUniqueInstance(){
        return uniqueInstance;
    }
}

測試:

public class Test {

    @org.junit.Test
    public void test6() {
        Object singleton1=Singleton6.Singleton.getUniqueInstance();
        Object singleton2=Singleton6.Singleton.getUniqueInstance();
        System.out.println(singleton1);
        System.out.println(singleton2);
    }
}

結果:

java.lang.Object@514713
java.lang.Object@514713

將Object換成我們需要實現單例模式的類型即可。

如果不使用枚舉來實現單例模式,會出現反射×××,因爲通過 setAccessible() 方法可以將私有構造函數的訪問級別設置爲 public,然後調用構造函數從而實例化對象。如果要防止這種×××,需要在構造函數中添加防止實例化第二個對象的代碼。

從上面的討論可以看出,解決序列化和反射×××很麻煩,而枚舉實現不會出現這兩種問題,所以說枚舉實現單例模式是最佳實踐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章