如何建立完美的單例模式?

原文做者: Ankit Sinhalhtml

原文地址:How to make the perfect Singleton?java

譯者: sunluyaoandroid

設計模式在軟件開發者中十分受歡迎。設計模式是對於常見軟件問題的良好解決方案。單例模式是 Java 中建立型設計模式的一種。數據庫

單例模式的目的是什麼?

單例類的目的是控制對象建立,約束對象的數量有且只有一個。單例模式只容許有一個入口來建立類實例。設計模式

由於只有一個單例類實例,任何單例類的實例都將只會產生一個類,就像靜態域同樣。當你須要控制資源的時候,例如在數據庫鏈接或者使用 sockets ,單例模式是很是有用的。api

這看起來是一個很簡單的設計模式,可是當咱們真正去實現的時候,會帶來許多的實現問題。單例模式的實如今開發者當中老是存在必定爭議。如今,咱們將會討論一下如何建立一個單例類以完成下列目的:安全

限制類的實例而且保證在 JVM 中只存在一個類實例。微信

讓咱們在 Java 中建立單例類並在不一樣的狀況下進行測試。多線程

建立單例類

爲了實現單例類,最簡單方法是把構造器變爲 private。有兩種初始化方法。oracle

餓漢式

餓漢式初始化,單例類的實例在類加載時被建立,這是建立單例類最簡單的方法。

經過將構造器聲明爲 private ,不容許其餘類來建立單例類實例。取而代之的是,建立一個靜態方法(一般命名爲 getInstance)來提供建立類實例的惟一入口。

public class SingletonClass {

	private static volatile SingletonClass sSoleInstance = new SingletonClass();

	//private constructor.
	private SingletonClass(){}

	public static SingletonClass getInstance() {
    	return sSoleInstance;
	}
}
複製代碼

這種方法有一個缺陷,即便在程序沒有使用到它的時候,實例已經被建立了。當你建立數據庫鏈接或者 socket 時,這可能成爲一個至關大的問題,會致使內存泄漏問題。解決方法是當須要的時候再建立實例,咱們稱之爲懶漢式初始化。

懶漢式

與餓漢式相反,你在 getInstance() 方法中初始化類實例。方法中將會判斷類實例是否已經建立,若是已經存在,將返回舊的實例,反之在 JVM 中建立新的實例並返回。

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	private SingletonClass(){}  //private constructor.

	public static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        sSoleInstance = new SingletonClass();
    	}

    return sSoleInstance;
	}
}
複製代碼

咱們都知道在 Java 中,若是兩個對象是相同的,那麼他們的 hashCode 也是相同的。讓咱們測試一下,若是上面的單例類都正確實現,那麼將會返回一樣的哈希。

public class SingletonTester {
	public static void main(String[] args) {
    	//Instance 1
    	SingletonClass instance1 = SingletonClass.getInstance();

    	//Instance 2
    	SingletonClass instance2 = SingletonClass.getInstance();

    	//now lets check the hash key.
    	System.out.println("Instance 1 hash:" + instance1.hashCode());
    	System.out.println("Instance 2 hash:" + instance2.hashCode());  
	}
}
複製代碼

下面是輸出日誌:

15:04:341 I/System.out: Instance 1 hash:247127865
15:04:342 I/System.out: Instance 2 hash:247127865
複製代碼

能夠看到兩個實例擁有一樣的 hashCode。因此,這就意味着上面的代碼建立了完美的單例類,是嗎?不。

讓單例類反射安全

在上面的單例類中,經過反射能夠建立不止一個實例。 Java Reflection 是一個在運行時檢測或者修改類的運行時行爲的過程。經過在運行時修改構造器的可見性並經過構造器建立實例能夠產生新的單例類實例。運行下面的代碼,單例類還存在嗎?

public class SingletonTester {
	public static void main(String[] args) {
    	//Create the 1st instance
    	SingletonClass instance1 = SingletonClass.getInstance();
    
    	//Create 2nd instance using Java Reflection API.
    	SingletonClass instance2 = null;
    	try {
        	Class<SingletonClass> clazz = SingletonClass.class;
        	Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
        	cons.setAccessible(true);
        	instance2 = cons.newInstance();
    	} catch (NoSuchMethodException | 	InvocationTargetException | 	IllegalAccessException | 	InstantiationException e) {
        	e.printStackTrace();
    	}

    	//now lets check the hash key.
    	System.out.println("Instance 1 hash:" + instance1.hashCode());
    	System.out.println("Instance 2 hash:" + instance2.hashCode());
	}
}
複製代碼

下面是輸出日誌:

15:21:48.216 I/System.out: Instance 1 hash:51110277
15:21:48.216 I/System.out: Instance 2 hash:212057050
複製代碼

每個實例都有不一樣的 hashCode。顯然這個單例類沒法經過測試。

解決方案:

爲了預防反射致使的單例失敗,當構造器已經初始化而且其餘類再次初始化時,拋出一個運行時異常。讓咱們更新 SingletonClass.java

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){
   
    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	} 

	public static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        	sSoleInstance = new SingletonClass();
    	}

    	return sSoleInstance;
	}
}
複製代碼

讓單例類線程安全

若是兩個線程幾乎同時嘗試初始化單例類,將會發生什麼?讓咱們測試下面的代碼,兩個線程幾乎同時被建立而且調用 getInstance()

public class SingletonTester {
	public static void main(String[] args) {
    	//Thread 1
    	Thread t1 = new Thread(new Runnable() {
        	@Override
        	public void run() {
            	SingletonClass instance1 = SingletonClass.getInstance();
            	System.out.println("Instance 1 hash:" + instance1.hashCode());
        	}
    	});

    	//Thread 2
    	Thread t2 = new Thread(new Runnable() {
        	@Override
        	public void run() {
            	SingletonClass instance2 = SingletonClass.getInstance();
            	System.out.println("Instance 2 hash:" + instance2.hashCode());
        	}
    	});

    	//start both the threads
    	t1.start();
    	t2.start();
	}
}	
複製代碼

若是你屢次運行這些代碼,有時你會發現不一樣的線程建立了不一樣的實例。

16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
複製代碼

這說明了你的單例類不是線程安全的。全部的線程同時調用 getInstance()方法,sSoleInstance == null 條件對全部線程返回值,因此兩個不一樣的實例被建立。這打破了單例原則。

解決方案

同步 getInstance() 方法

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){
   
    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	} 

	public synchronized static SingletonClass getInstance(){
    	if (sSoleInstance == null){ //if there is no instance available... create new one
        	sSoleInstance = new SingletonClass();
    	}

    	return sSoleInstance;
	}
}
複製代碼

在咱們同步 getInstance() 方法以後,第二個線程必須等到第一個線程執行完 getInstance() 方法以後才能執行,這就保證了線程安全。

可是,這個方法一樣有一些缺點:

  • 鎖的開銷致使運行變慢
  • 實例變量初始化以後的同步操做時沒必要要的

雙檢查鎖

使用 雙檢查鎖 方法建立實例能夠克服上面的問題。

這這種方法中,當實例爲空時,在同步代碼塊中建立單例類,這樣只有當 sSoleInstance 爲空時,同步代碼塊纔會執行,避免了沒必要要的同步操做。

public class SingletonClass {

	private static SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	}

	public static SingletonClass getInstance() {
    	//Double check locking pattern
    	if (sSoleInstance == null) { //Check for the first time
      
        	synchronized (SingletonClass.class) {   //Check for the second time.
          	//if there is no instance available... create new one
          	if (sSoleInstance == null) sSoleInstance = new SingletonClass();
        	}
    	}

    	return sSoleInstance;
	}
}
複製代碼

使用 volatile 關鍵字

表面上看,這個方法看起來很完美,你只須要付出一次靜態代碼塊的代價。可是除非你使用 volatile 關鍵字,不然單例仍然會被打破。

沒有 volatile 修飾符,另外一個線程可能在變量 sSoleInstance 正在初始化還沒有完成時引用它。可是經過 volatile 的保證 happens-before 關係,全部對於 sSoleInstance 變量的寫操做都會在讀操做以前發生。

public class SingletonClass {

	private static volatile SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
        	throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
    	}
	}

	public static SingletonClass getInstance() {
    	//Double check locking pattern
    	if (sSoleInstance == null) { //Check for the first time
      
        	synchronized (SingletonClass.class) {   //Check for the second time.
          	//if there is no instance available... create new one
          	if (sSoleInstance == null) sSoleInstance = new SingletonClass();
        	}
    	}

    	return sSoleInstance;
	}
}
複製代碼

如今上面的單例類是線程安全的。在多線程應用環境中(好比安卓應用)保證單例類的線程安全是必需的。

讓單例類序列化安全

在分佈式系統中,有些狀況下你須要在單例類中實現 Serializable 接口。這樣你能夠在文件系統中存儲它的狀態而且在稍後的某一時間點取出。

讓咱們測試一個這個單例類在序列化和反序列化以後是否仍然保持單例。

public class SingletonTester {
	public static void main(String[] args) {
  
  		try {
    	    SingletonClass instance1 = SingletonClass.getInstance();
    	    ObjectOutput out = null;

    	    out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
    	    out.writeObject(instance1);
    	    out.close();

    	    //deserialize from file to object
    	    ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
    	    SingletonClass instance2 = (SingletonClass) in.readObject();
    	    in.close();

    	    System.out.println("instance1 hashCode=" + instance1.hashCode());
    	    System.out.println("instance2 hashCode=" + instance2.hashCode());

    	} catch (IOException | ClassNotFoundException e) {
    	    e.printStackTrace();
    	}
  }
}


16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
複製代碼

能夠看到實例的 hashCode 是不一樣的,違反了單例原則。序列化單例類以後,當咱們反序列化時,會建立一個新的類實例。爲了預防另外一個實例的產生,你須要提供 readResolve() 方法的實現。readResolve()代替了從流中讀取對象。這就確保了在序列化和反序列化的過程當中沒人能夠建立新的實例。

public class SingletonClass implements Serializable {

	private static volatile SingletonClass sSoleInstance;

	//private constructor.
	private SingletonClass(){

    	//Prevent form the reflection api.
    	if (sSoleInstance != null){
    	    throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
   	 }
	}

	public static SingletonClass getInstance() {
    	if (sSoleInstance == null) { //if there is no instance available... create new one
    	    synchronized (SingletonClass.class) {
    	        if (sSoleInstance == null) 	sSoleInstance = new SingletonClass();
    	    }
   	 }

    	return sSoleInstance;
	}

	//Make singleton from serialize and deserialize operation.
	protected SingletonClass readResolve() {
    	return getInstance();
	}
}
複製代碼

結論

在文章的最後,你能夠建立線程,反射和序列化安全的單例類,但這仍然不是完美的單例,你可使用克隆或者多個類加載器來建立不止一個實例。可是對於大多數應用,上面的實現方法已經能夠很好的工做了。

文章同步更新於微信公衆號: 秉心說 , 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!

相關文章
相關標籤/搜索