1. 程式人生 > >深入理解單例模式——只有一個例項

深入理解單例模式——只有一個例項

目錄:

前言

初遇設計模式在上個寒假,當時把每個設計模式過了一遍,對設計模式有了一個最初級的瞭解。這個學期借了幾本設計模式的書籍看,聽了老師的設計模式課,對設計模式算是有個更進一步的認識。後面可能會不定期更新一下自己對於設計模式的理解。每個設計模式看似很簡單,實則想要在一個完整的系統中應用還是非常非常難的。然後我的水品也非常非常有限,程式碼量也不是很多,只能通過閱讀書籍、思考別人的編碼經驗以及結合自己的編碼過程中遇到的問題來總結。

怎麼用->怎麼用才好->怎麼與其他模式結合使用,我想這是每個開發人員都需要逾越的一道鴻溝。

一 單例模式簡介

1.1 定義

保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。

1.2 為什麼要用單例模式呢?

在我們的系統中,有一些物件其實我們只需要一個,比如說:執行緒池、快取、對話方塊、登錄檔、日誌物件、充當印表機、顯示卡等裝置驅動程式的物件。事實上,這一類物件只能有一個例項,如果製造出多個例項就可能會導致一些問題的產生,比如:程式的行為異常、資源使用過量、或者不一致性的結果。

簡單來說使用單例模式可以帶來下面幾個好處:

  • 對於頻繁使用的物件,可以省略建立物件所花費的時間,這對於那些重量級物件而言,是非常可觀的一筆系統開銷;
  • 由於 new 操作的次數減少,因而對系統記憶體的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間。

1.3 為什麼不使用全域性變數確保一個類只有一個例項呢?

我們知道全域性變數分為靜態變數和例項變數,靜態變數也可以保證該類的例項只存在一個。
只要程式載入了類的位元組碼,不用建立任何例項物件,靜態變數就會被分配空間,靜態變數就可以被使用了。

但是,如果說這個物件非常消耗資源,而且程式某次的執行中一直沒用,這樣就造成了資源的浪費。利用單例模式的話,我們就可以實現在需要使用時才建立物件,這樣就避免了不必要的資源浪費。 不僅僅是因為這個原因,在程式中我們要儘量避免全域性變數的使用,大量使用全域性變數給程式的除錯、維護等帶來困難。

二 單例的模式的實現

通常單例模式在Java語言中,有兩種構建方式:

  • 餓漢方式。指全域性的單例例項在類裝載時構建
  • 懶漢方式。指全域性的單例例項在第一次被使用時構建。

不管是那種建立方式,它們通常都存在下面幾點相似處:

  • 單例類必須要有一個 private 訪問級別的建構函式,只有這樣,才能確保單例不會在系統中的其他程式碼內被例項化;
  • instance 成員變數和 uniqueInstance 方法必須是 static 的。

2.1 餓漢方式(執行緒安全)

    public class Singleton {
       //在靜態初始化器中建立單例例項,這段程式碼保證了執行緒安全
        private static Singleton uniqueInstance = new Singleton();
        //Singleton類只有一個構造方法並且是被private修飾的,所以使用者無法通過new方法建立該物件例項
        private Singleton(){}
        public static Singleton getInstance(){
            return uniqueInstance;
        }
    }

所謂 “餓漢方式” 就是說JVM在載入這個類時就馬上建立此唯一的單例例項,不管你用不用,先建立了再說,如果一直沒有被使用,便浪費了空間,典型的空間換時間,每次呼叫的時候,就不需要再判斷,節省了執行時間。

2.2 懶漢式(非執行緒安全和synchronized關鍵字執行緒安全版本 )

public class Singleton {  
      private static Singleton uniqueInstance;  
      private Singleton (){
      }   
      //沒有加入synchronized關鍵字的版本是執行緒不安全的
      public static Singleton getInstance() {
          //判斷當前單例是否已經存在,若存在則返回,不存在則再建立單例
	      if (uniqueInstance == null) {  
	          uniqueInstance = new Singleton();  
	      }  
	      return uniqueInstance;  
      }  
 }

所謂 “ 懶漢式” 就是說單例例項在第一次被使用時構建,而不是在JVM在載入這個類時就馬上建立此唯一的單例例項。

但是上面這種方式很明顯是執行緒不安全的,如果多個執行緒同時訪問getInstance()方法時就會出現問題。如果想要保證執行緒安全,一種比較常見的方式就是在getInstance() 方法前加上synchronized關鍵字,如下:

      public static synchronized Singleton getInstance() {  
	      if (instance == null) {  
	          uniqueInstance = new Singleton();  
	      }  
	      return uniqueInstance;  
      }  

我們知道synchronized關鍵字偏重量級鎖。雖然在JavaSE1.6之後synchronized關鍵字進行了主要包括:為了減少獲得鎖和釋放鎖帶來的效能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升。

但是在程式中每次使用getInstance() 都要經過synchronized加鎖這一層,這難免會增加getInstance()的方法的時間消費,而且還可能會發生阻塞。我們下面介紹到的 雙重檢查加鎖版本 就是為了解決這個問題而存在的。

2.3 懶漢式(雙重檢查加鎖版本)

利用雙重檢查加鎖(double-checked locking),首先檢查是否例項已經建立,如果尚未建立,“才”進行同步。這樣以來,只有一次同步,這正是我們想要的效果。

public class Singleton {

    //volatile保證,當uniqueInstance變數被初始化成Singleton例項時,多個執行緒可以正確處理uniqueInstance變數
    private volatile static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
       //檢查例項,如果不存在,就進入同步程式碼塊
        if (uniqueInstance == null) {
            //只有第一次才徹底執行這裡的程式碼
            synchronized(Singleton.class) {
               //進入同步程式碼塊後,再檢查一次,如果仍是null,才建立例項
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

很明顯,這種方式相比於使用synchronized關鍵字的方法,可以大大減少getInstance() 的時間消費。

注意: 雙重檢查加鎖版本不適用於1.4及更早版本的Java。
1.4及更早版本的Java中,許多JVM對於volatile關鍵字的實現會導致雙重檢查加鎖的失效。

2.4 懶漢式(登記式/靜態內部類方式)

靜態內部實現的單例是懶載入的且執行緒安全。

只有通過顯式呼叫 getInstance 方法時,才會顯式裝載 SingletonHolder 類,從而例項化 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;  
    }  
}   

2.5 餓漢式(列舉方式)

這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。 它更簡潔,自動支援序列化機制,絕對防止多次例項化 (如果單例類實現了Serializable介面,預設情況下每次反序列化總會建立一個新的例項物件,關於單例與序列化的問題可以檢視這一篇文章《單例與序列化的那些事兒》),同時這種方式也是《Effective Java 》以及《Java與模式》的作者推薦的方式。

public enum Singleton {
	 //定義一個列舉的元素,它就是 Singleton 的一個例項
    INSTANCE;  
    
    public void doSomeThing() {  
	     System.out.println("列舉方法實現單例");
    }  
}

使用方法:

public class ESTest {

	public static void main(String[] args) {
		Singleton singleton = Singleton.INSTANCE;
		singleton.doSomeThing();//output:列舉方法實現單例

	}

}

《Effective Java 中文版 第二版》

這種方法在功能上與公有域方法相近,但是它更加簡潔,無償提供了序列化機制,絕對防止多次例項化,即使是在面對複雜序列化或者反射攻擊的時候。雖然這種方法還沒有廣泛採用,但是單元素的列舉型別已經成為實現Singleton的最佳方法。 —-《Effective Java 中文版 第二版》

《Java與模式》

《Java與模式》中,作者這樣寫道,使用列舉來實現單例項控制會更加簡潔,而且無償地提供了序列化機制,並由JVM從根本上提供保障,絕對防止多次例項化,是更簡潔、高效、安全的實現單例的方式。

2.6 總結

我們主要介紹到了以下幾種方式實現單例模式:

  • 餓漢方式(執行緒安全)
  • 懶漢式(非執行緒安全和synchronized關鍵字執行緒安全版本)
  • 懶漢式(雙重檢查加鎖版本)
  • 懶漢式(登記式/靜態內部類方式)
  • 餓漢式(列舉方式)

參考:

《Head First 設計模式》

《Effective Java 中文版 第二版》

我是Snailclimb,一個以架構師為5年之內目標的小小白。
歡迎關注我的微信公眾號:“Java面試通關手冊”(一個有溫度的微信公眾號,期待與你共同進步~~~堅持原創,分享美文,分享各種Java學習資源):

我的公眾號