單例模式
作用:確保一個類只有一個實例,並提供該實例的全局訪問點
結構:使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。
私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。
實現:
(一)懶漢式(線程不安全)
以下實現中,私有靜態變量 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();
這段代碼其實是分爲三步執行。
分配內存空間
初始化對象
將 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,然後調用構造函數從而實例化對象。如果要防止這種×××,需要在構造函數中添加防止實例化第二個對象的代碼。
從上面的討論可以看出,解決序列化和反射×××很麻煩,而枚舉實現不會出現這兩種問題,所以說枚舉實現單例模式是最佳實踐。