建立對象——單例(Singleton)模式

 

單例(Singleton)模式

 
保證一個類在系統裏只能有一個對象被實例化。
 
如:緩存池、數據庫鏈接池、線程池、一些應用服務實例等。
 
難點:在多線程環境中,保證明例的惟一性。
 
 

最簡單的單例模式:

  1. 保證該類構造方法是私有的,外部沒法建立該類型的對象;
  2. 提供一個全局訪問點,方便給客戶對象提供對此單例對象的使用;
 
public class Singleton {
    /**
     * 私有變量,外界沒法訪問
     * 能夠定義 public 類型 instance變量,把屬性直接暴露給客戶對象,則不必實現getInstance()方法
     * 可是可讀性下降,並且直接暴露實例變量的名字給客戶程序,會增長代碼的耦合度
     */
    private static Singleton instance = new Singleton();
 
    static {
        //...
    }
 
 
    // 惟一的 private構造方法,客戶對象沒法建立該對象實例
    private Singleton() {
 
    }
 
 
    // 全局訪問點
    public static Singleton getInstance() {
        return instance;
    }
}
 
// 客戶使用單例模式代碼
Singleton singleton = Singleton.getInstance();

 

    若是該實例須要比較複雜的初始化過程時,把這個過程應該寫在 static{ ... }代碼快中。
    注意:此實現是線程安全的,當對個線程同時去訪問該類的 getInstance( ) 方法時,不會初始化多個不一樣的對象,這是由於,JVM 在加載此類時,對於 static 屬性的初始化只能由一個線程執行且僅一次。
 
 

進階:

    Statci 在加載類時就會被初始化,出於性能等方面的考慮,咱們但願延遲實例化單例對象,只有在第一次使用該類的實例時纔去實例化。
 
延遲建立
 
public static Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}
    咱們把單例的實例化過程移至 getInstance( )方法,而不是在加載類時預先建立,當訪問此方法時,首先判斷該實例是否是已經被實例化過了,若是已被初始化,則直接返回這個對象的引用;不然,建立這個實例並初始化,最後返回這個對象引用。
 
    使用 if (instance == null) 判斷是否實例化完成了,此方法不是線程安全的。
 
 
線程安全
    在高併發的環境中,getInstance( ) 方法將返回多個指向不一樣的該類實例。
 
  Thread 1 Thread 2
1 if (instance == null)  
2   if (instance == null)
3 Singleton instance = new Singleton();  
4   Singleton instance = new Singleton();
5 return instance;  
6   return instance;
 
    在時刻 1和 2,因爲尚未建立單例對象,Thread 1 和 Thread 2都會進入建立單例實例的代碼塊分別建立實例。在時刻 3 ,Thread 1建立了一個實例對象,可是 Thread 2此時已沒法知道,因而繼續建立一個新的實例對象,致使這兩個線程持有的實例並不是爲同一個。
    更爲糟糕的是,在沒有自動內存回收機制的語言平臺上運行這樣的單例模式,如:C++,覺得咱們認爲建立了一個單例實例,忽略了其餘線程所產生的對象,不會手動去回收它們,從而引發內存泄漏。
 
爲了解決這個問題,咱們給次方法添加 關鍵字,代碼以下:
 
  
public   static   synchronized  Singleton getInstance() {
     if  ( instance  ==  null ) {
         instance  =  new  Singleton();
     }
     return   instance ; 
}

 

 
    這樣,再多的線程訪問都只會實例化一個單例對象,實現了多線程的安全訪問,可是在多線程高併發訪問的狀況下,給此方法加上 ynchronized 關鍵字會是得性能大不如前。
 
 
如何建立併發訪問效率高的單例Double-Check Locking
 
    仔細分析發現,使用 synchronized 關鍵字對整個 getInstance( ) 方法進行同步是沒有必要的:咱們只要保證明例化這個對象的那段邏輯被一個線程執行就能夠了,而返回引用的那段代碼是沒有必要同步的。更改去下:
public   static  Singleton getInstance() {
     if  ( instance  ==  null ) {
         synchronized  (Singleton. class ) {
             if  ( instance  ==  null ) {
                 instance  =  new  Singleton();
            }
        }
    }
    return   instance ;
}

 

 
    在 getInstance( )方法裏,首先判斷實例是否已經被建立了,若是尚未建立,首先使用 synchronized 同步實例代碼塊。在同步代碼塊裏,還須要再次檢查是否已經建立了此類的實例,這是由於:若是沒有第二次檢查,這時有兩個線程 Thread A 和 Thread B 同時進入該方法,它們都檢測到 instatnce 爲 null,無論哪個線程先佔據同步鎖,並建立實例對象,都不會阻止另一個線程繼續進入實例代碼塊從新建立實例對象,這樣,一樣會產生兩個實例對象。因此,咱們在同步的代碼塊裏,要進行第二次判斷,判斷該代碼是否已被建立。
 
 
注意:此程序只有在 JAVA 5及以上版本才能正常運行,在之前版本不能保證其正常運行。這是因爲 Java平臺的內存模式允許 out-of-order writes 引發的,假定有兩個線程,Thread 1 和 Thread 2,它們執行如下步驟:
     一、Thread 1發現 instatnce 沒有被實例化,它得到鎖,並去實例化此對象,JVM 允許在沒有徹底實例化完成時,instance 變量就指向此實例,由於這些步驟能夠是 out-of-order writes 的,此時 instance==null 爲 false,以前的版本即便用 volatile 關鍵字修飾也無效。
     二、在初始化完成以前,Thread 2 進入此方法,發現 instance 已經不爲 null了,Thread 2 便認爲該實例初始化完成了,使用這個未完成初始化的實例對象,則極可能引發系統的奔潰。
 
 
 
Initialization on demand holder
    要使用線程安全的延遲的單例初始化,還有一種方法,代碼以下:
public   class   LazyLoadedSingleton  {
     private  LazyLoadedSingleton {
        
    }
     private   static   class  LazyHolder{
         private   static   final  LazyLoadedSingleton  singletonInstatnce =  new  LazyLoadedSingleton();
    }
     public   static  LazyLoadedSingleton getInstance() {
         return  LazyHolder.singletonInstatnce;
    }
}

 

 

 
    當 JVM 加載 LazyLoadedSingleton   類時,因爲該類沒有 static 屬性,因此加載完成後便便可返回。只有第一次調用 getInstance( ) 方法時,JVM 纔會加載 LazyHolder 類,因爲它包含一個 static 屬性 singletonInstatnce,因此會首先初始化這個變量,這樣即實現了一個即保證線程安全又支持延遲加載的單例模式。
 
單例模式序列化應該注意的問題 Singleton 的序列化
 
    若是單例類實現了 Serializable接口,在默認狀況下,每次反序列化總會建立一個新的實例對象,這樣一個系統會出現多個對象使用。
    解決思路: readResolve( )方法在反序列化完成以前被執行,咱們在此方法裏替換掉反序列化出來的那個新的實例,讓其指向內存中的那個單例對象便可,代碼以下:
public   class  SerializableSingleton  implements  Serializable {
     private   static   final   long   serialVersionUID  = 4285441628073602932L;
     static  SerializableSingleton  singleton  =  new  SerializableSingleton();
     private  SerializableSingleton() {
    }
     private  Object  readResolve () {
         return   singleton ;
    }
}
    方法 readResovle( ) 直接返回 singleton單例,這樣,在內存中始終保持了一個惟一的單例對象。
 
 
 
思考:以上學習,討論的是在同一個 JVM中,保證一個類只有一個單例,若是在分佈式環境中,如何保證在整個應用(可能分佈在不一樣 JVM上)只有一個實例???
相關文章
相關標籤/搜索