大話設計模式(四)單例模式

大話設計模式(四)單例模式的優與劣

前言

    首先來明白一個問題。那就是在某些狀況下,有些對象,咱們僅僅需要一個就可以了。比方,一臺計算機上可以連好幾個打印機,但是這個計算機上的打印程序僅僅能有一個。這裏就可以經過單例模式來避免兩個打印做業同一時候輸出到打印機中。即在整個的打印過程當中我僅僅有一個打印程序的實例。html

    簡單說來,單例模式(也叫單件模式)的做用就是保證在整個應用程序的生命週期中,不論什麼一個時刻,單例類的實例都僅僅存在一個(固然也可以不存在)。java

    下圖是單例模式的結構圖。面試

    如下就來看一種狀況(這裏先假設個人應用程序是多線程應用程序),演示樣例代碼例如如下:數據庫

        

public static Singleton GetInstance()
        {  
            if (singleton == null)
            {
                singleton = new Singleton();
            }
            return singleton;
        }

  假設在一開始調用 GetInstance()時,是由兩個線程同一時候調用的(這種狀況是非常常見的),注意是同一時候,(或者是一個線程進入 if 推斷語句後但尚未實例化 Singleton 時。第二個線程到達。此時 singleton 仍是爲 null)這種話,兩個線程均會進入 GetInstance()。然後由於是第一次調用 GetInstance(),因此存儲在 Singleton 中的靜態變量 singleton null ,這種話,就會讓兩個線程均經過 if 語句的條件推斷。而後調用 new Singleton()了。這種話,問題就出來了,由於有兩個線程,因此會建立兩個實例。編程

    很是顯然,這便違法了單例模式的初衷了,設計模式

    那麼怎樣解決上面出現的這個問題(即多線程下使用單例模式時有可能會建立多個實例這一現象)呢?安全

  事實上,這個是很是好解決的,可以這樣思考這個問題:由於上面出現的問題中涉及到多個線程同一時候訪問這個 GetInstance(),那麼可以先將一個線程鎖定,而後等這個線程完畢之後,再讓其它的線程訪問 GetInstance()中的 if 段語句。演示樣例代碼例如如下:多線程

   

     public static Singleton GetInstance()
        {  
lock(syncRoot){
         if (singleton == null)
            {
                singleton = new Singleton();
            }
}
            return singleton;
        }

  但是假設這種話。每次調用GetInstance方法時都需要lock操做。影響性能。函數

 如下就來又一次改進前面 Demo 中的 Singleton 類,使其在多線程的環境下也可以實現單例模式的功能。post

    

public class Singleton
    {
        //定義一個私有的靜態全局變量來保存該類的惟一實例
        private static Singleton singleton;
        //定義一個靜態對象,且這個對象是在程序運行時建立的。
        private static object syncObject = new object();
        //構造函數必須是私有的,這樣在外部便沒法使用 new 來建立該類的實例
       private Singleton(){}
        //定義一個全局訪問點。設置爲靜態方法,則在類的外部便無需實例化就可以調用該方法
        public static Singleton GetInstance()
        {
            //這裏可以保證僅僅實例化一次,即在第一次調用時實例化。之後調用便不會再實例化
            //第一重 singleton == null
            if (singleton == null)
            {
                lock (syncObject)
                {
                  //第二重 singleton == null
                    if (singleton == null)
                    {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
}

  上面的就是改進後的代碼,可以看到在類中有定義了一個靜態的僅僅讀對象syncObject,這裏需要說明的是,爲什麼還要建立一個 syncObject 靜態僅僅讀對象呢?

  由於提供給 lock keyword的參數必須爲基於引用類型的對象。該對象用來定義鎖的範圍,因此這個引用類型的對象總不能爲 null 吧,而一開始的時候。singleton null 。因此是沒法實現加鎖的。因此必需要再建立一個對象即 syncObject 來定義加鎖的範圍。

  還有要解釋一下的就是在 GetInstance()中,我爲何要在 if 語句中使用兩次推斷 singleton == null ,這裏涉及到一個名詞 Double-Check Locking ,也就是雙重檢查鎖定。爲什麼要使用雙重檢查鎖定呢?

  考慮這樣一種狀況,就是有兩個線程同一時候到達,即同一時候調用 GetInstance(),此時由於 singleton == null ,因此很是明顯,兩個線程都可以經過第一重的 singleton == null 。進入第一重 if 語句後。由於存在鎖機制,因此會有一個線程進入 lock 語句並進入第二重 singleton == null ,而另外的一個線程則會在 lock 語句的外面等待。

  而當第一個線程運行完 new  Singleton()語句後,便會退出鎖定區域。此時,第二個線程便可以進入 lock 語句塊。此時,假設沒有第二重 singleton == null 的話,那麼第二個線程仍是可以調用 new  Singleton()語句,這樣第二個線程也會建立一個 Singleton 實例,這樣也仍是違背了單例模式的初衷的,因此這裏必需要使用雙重檢查鎖定

  細心的朋友必定會發現,假設我去掉第一重 singleton == null ,程序仍是可以在多線程下完善的運行的,考慮在沒有第一重 singleton == null 的狀況下。當有兩個線程同一時候到達,此時,由於 lock 機制的存在,第一個線程會進入 lock 語句塊。並且可以順利運行 new Singleton()。當第一個線程退出 lock 語句塊時, singleton 這個靜態變量已不爲 null 了。因此當第二個線程進入 lock 時。仍是會被第二重 singleton == null 擋在外面,而沒法運行 new Singleton(),因此在沒有第一重 singleton == null 的狀況下。也是可以實現單例模式的?那麼爲何需要第一重 singleton == null 呢?

  這裏就涉及一個性能問題了。由於對於單例模式的話,new Singleton()僅僅需要運行一次就 OK 了。而假設沒有第一重 singleton == null 的話。每一次有線程進入 GetInstance()時,均會運行鎖定操做來實現線程同步,這是很是耗費性能的,而假設我加上第一重 singleton == null 的話,那麼就僅僅有在第一次,也就是 singleton ==null 成立時的狀況下運行一次鎖定以實現線程同步,而之後的話,便僅僅要直接返回 Singleton 實例就 OK 了而根本無需再進入 lock 語句塊了,這樣就可以解決由線程同步帶來的性能問題了。

  好,關於多線程下單例模式的實現的介紹就到這裏了,但是,關於單例模式的介紹還沒完。

單例的三種實現方式    

   如下將要介紹的是懶漢式單例和餓漢式單例

懶漢式單例

  何爲懶漢式單例呢,可以這樣理解。單例模式呢,其在整個應用程序的生命週期中僅僅存在一個實例,懶漢式呢,就是這個單例類的這個惟一實例是在第一次使用 GetInstance()時實例化的,假設不調用 GetInstance()的話,這個實例是不會存在的,即爲 null

    形象點說呢,就是你不去動它的話。它本身是不會實例化的,因此可以稱之爲懶漢。

  事實上呢,我前面在介紹單例模式的這幾個 Demo 中都是使用的懶漢式單例,看如下的 GetInstance()方法就明白了:

private static volatile TestSingleton instance = null;
        public static Singleton GetInstance()
        {
            if (singleton == null)
            {
                lock (syncObject)
// synchronized (TestSingleton.class)
                {
                    if (singleton == null)
                    {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }

   從上面的這個 GetInstance()中可以看出這個單例類的惟一實例是在第一次調用 GetInstance()時實例化的,因此此爲懶漢式單例。

   另外。可以看到裏面加了volatilekeyword來聲明單例對象,既然synchronized已經起到了多線程下原子性、有序性、可見性的做用。爲何還要加volatile呢?見參考文獻。

   雙重檢測鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型贊成所謂的「無序寫入」,這也是失敗的一個主要緣由。所以,爲了杜絕「無序寫入」的出現,使用voaltilekeyword。

餓漢式單例

   上面介紹了懶漢式單例,到這裏來理解餓漢式單例的話,就easy多了。懶漢式單例是不會主動實例化單例類的惟一實例的,而餓漢式的話。則恰好相反,他會以靜態初始化的方式在本身被載入時就將本身實例化。

   如下就來看一看餓漢式單例類。

//餓漢式單例類.在類初始化時,已經自行實例化
 public class Singleton1 {
     //私有的默認構造器
     private Singleton1() {}
     //已經自行實例化
     private static final Singleton1 single = new Singleton1();
     //靜態工廠方法
     public static Singleton1 getInstance() {
         return single;
     }
 }

  上面的餓漢式單例類中可以看到。當整個類被載入的時候,就會自行初始化 singleton 這個靜態僅僅讀變量。而非在第一次調用 GetInstance()時再來實例化單例類的惟一實例,因此這就是一種餓漢式的單例類。

登記式單例類(可忽略)

import java.util.HashMap;
 import java.util.Map;
 //登記式單例類.
 //類似Spring裏面的方法。將類名註冊。下次從裏面直接獲取。

public class Singleton3 { private static Map<String,Singleton3> map = new HashMap<String,Singleton3>(); static{ Singleton3 single = new Singleton3(); map.put(single.getClass().getName(), single); } //保護的默認構造器 protected Singleton3(){} //靜態工廠方法,返還此類唯一的實例 public static Singleton3 getInstance(String name) { if(name == null) { name = Singleton3.class.getName(); System.out.println("name == null"+"--->name="+name); } if(map.get(name) == null) { try { map.put(name, (Singleton3) Class.forName(name).newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return map.get(name); } //一個示意性的商業方法 public String about() { return "Hello, I am RegSingleton."; } public static void main(String[] args) { Singleton3 single3 = Singleton3.getInstance(null); System.out.println(single3.about()); } }

     登記式單例實際上維護了一組單例類的實例,將這些實例存放在一個Map(登記薄)中。對於已經登記過的實例,則從Map直接返回。對於沒有登記的。則先登記。而後返回。

 
    這裏我對登記式單例標記了可忽略。個人理解來講,首先它用的比較少,另外事實上內部實現仍是用的餓漢式單例,由於當中的static方法塊,它的單例在類被裝載的時候就被實例化了。     

     好,到這裏,就真正的把單例模式介紹完了。在此呢再總結一下單例類需要注意的幾點:

      1、單例模式是用來實現在整個程序中僅僅有一個實例的。

     2、單例類的構造函數必須爲私有,同一時候單例類必須提供一個全局訪問點。

     3、單例模式在多線程下的同步問題和性能問題的解決。

      4、懶漢式和餓漢式單例類。

餓漢式與懶漢式的差異

     從速度和反應時間角度來說,非延遲載入(又稱餓漢式)好;從資源利用效率上說。延遲載入(又稱懶漢式)好。

     餓漢式天生就是線程安全的,可以直接用於多線程而不會出現故障。懶漢式自己是非線程安全的。爲了實現線程安全需附加語句。

     餓漢式在類建立的同一時候就實例化一個靜態對象出來。不管以後會不會使用這個單例,都會佔領必定的內存,但是對應的,在第一次調用時速度也會更快,由於其資源已經初始化完畢。

     而懶漢式顧名思義,會延遲載入。在第一次使用該單例的時候纔會實例化對象出來,第一次調用時要作初始化,假設要作的工做比較多,性能上會有些延遲,以後就和餓漢式同樣了。

單例對象做配置信息管理時可能會帶來的幾個同步問題

     1.在多線程環境下,單例對象的同步問題主要體現在兩個方面,單例對象的初始化和單例對象的屬性更新。

     本文描寫敘述的方法有例如如下假設:

     a. 單例對象的屬性(或成員變量)的獲取,是經過單例對象的初始化實現的。

也就是說,在單例對象初始化時。會從文件或數據庫中讀取最新的配置信息。

     b. 其它對象不能直接改變單例對象的屬性。單例對象屬性的變化來源於配置文件或配置數據庫數據的變化。

1.1單例對象的初始化

 首先,討論一下單例對象的初始化同步。單例模式的一般處理方式是。在對象中有一個靜態成員變量,其類型就是單例類型自己;假設該變量爲null,則建立該單例類型的對象,並將該變量指向這個對象;假設該變量不爲null。則直接使用該變量。   

 這種處理方式在單線程的模式下可以很是好的運行;但是在多線程模式下,可能產生問題。

假設第一個線程發現成員變量爲null,準備建立對象。這是第二個線程同一時候也發現成員變量爲null,也會建立新對象。這就會形成在一個JVM中有多個單例類型的實例。

假設這個單例類型的成員變量在運行過程當中變化。會形成多個單例類型實例的不一致,產生一些很是奇怪的現象。

好比。某服務進程經過檢查單例對象的某個屬性來中止多個線程服務,假設存在多個單例對象的實例,就會形成部分線程服務中止,部分線程服務不能中止的狀況(此時可考慮使用雙重鎖安全機制)

1.2單例對象的屬性更新

 一般,爲了實現配置信息的實時更新,會有一個線程不停檢測配置文件或配置數據庫的內容,一旦發現變化,就更新到單例對象的屬性中。在更新這些信息的時候,很是可能還會有其它線程正在讀取這些信息,形成意想不到的後果。

仍是以經過單例對象屬性中止線程服務爲例。假設更新屬性時讀寫不一樣步。可能訪問該屬性時這個屬性正好爲空(null),程序就會拋出異常。

   如下是解決方法。

//單例對象的初始化同步
public class GlobalConfig {
    private static GlobalConfig instance = null;
    private Vector properties = null;
    private GlobalConfig() {
      //Load configuration information from DB or file
      //Set values for properties
    }
    private static synchronized void syncInit() {
      if (instance == null) {
        instance = new GlobalConfig();
      }
    }
    public static GlobalConfig getInstance() {
      if (instance == null) {
        syncInit();
      }
      return instance;
    }
    public Vector getProperties() {
      return properties;
    }
  }

    這種處理方式儘管引入了同步代碼。但是由於這段同步代碼僅僅會在最開始的時候運行一次或屢次,因此對整個系統的性能不會有影響。

 參照讀者/寫者的處理方式,設置一個讀計數器,每次讀取配置信息前。將計數器加1,讀完後將計數器減1.僅僅有在讀計數器爲0時,才幹更新數據,同一時候要堵塞所有讀屬性的調用。

    代碼例如如下:

public class GlobalConfig {
 private static GlobalConfig instance;
 private Vector properties = null;
 private boolean isUpdating = false;
 private int readCount = 0;
 private GlobalConfig() {
   //Load configuration information from DB or file
      //Set values for properties
 }
 private static synchronized void syncInit() {
  if (instance == null) {
   instance = new GlobalConfig();
  }
 }
 public static GlobalConfig getInstance() {
  if (instance==null) {
   syncInit();
  }
  return instance;
 }
 public synchronized void update(String p_data) {
  syncUpdateIn();
  //Update properties
 }
 private synchronized void syncUpdateIn() {
  while (readCount > 0) {
   try {
    wait();
   } catch (Exception e) {
   }
  }
 }
 private synchronized void syncReadIn() {
  readCount++;
 }
 private synchronized void syncReadOut() {
  readCount--;
  notifyAll();
 }
 public Vector getProperties() {
  syncReadIn();
  //Process data
  syncReadOut();
  return properties;
 }
}

  採用"影子實例"的辦法。詳細說。就是在更新屬性時,直接生成還有一個單例對象實例。這個新生成的單例對象實例將從數據庫或文件裏讀取最新的配置信息;而後將這些配置信息直接賦值給舊單例對象的屬性。

public class GlobalConfig {
    private static GlobalConfig instance = null;
    private Vector properties = null;
    private GlobalConfig() {
      //Load configuration information from DB or file
      //Set values for properties
    }
    private static synchronized void syncInit() {
      if (instance = null) {
        instance = new GlobalConfig();
      }
    }
    public static GlobalConfig getInstance() {
      if (instance = null) {
        syncInit();
      }
      return instance;
    }
    public Vector getProperties() {
      return properties;
    }
    public void updateProperties() {
      //Load updated configuration information by new a GlobalConfig object
      GlobalConfig shadow = new GlobalConfig();
      properties = shadow.getProperties();
    }
  }

  注意:在更新方法中,經過生成新的GlobalConfig的實例,從文件或數據庫中獲得最新配置信息,並存放到properties屬性中。上面兩個方法比較起來,第二個方法更好,首先,編程更簡單;其次。沒有那麼多的同步操做,對性能的影響也不大。

全局變量和單例模式的差異

     首先,全局變量就是對一個對象的靜態引用。全局變量確實可以提供單例模式實現的全局訪問這個功能。但是。它並不能保證應用程序中僅僅有一個實例。

     同一時候。在編碼規範中,也明白指出。應該要少用全局變量,由於過多的使用全局變量,會形成代碼難讀。

     還有就是全局變量並不能實現繼承(儘管單例模式在繼承上也不能很是好的處理,但是仍是可以實現繼承的)而單例模式的話,其在類中保存了它的惟一實例。這個類,它可以保證僅僅能建立一個實例,同一時候,它還提供了一個訪問該惟一實例的全局訪問點。

單例模式的優與劣

  上面嗶嗶了這麼多,言歸正傳。回到「單例模式的利與弊」問題上來。

總結例如如下:

主要長處

     1、提供了對惟一實例的受控訪問。

     2、由於在系統內存中僅僅存在一個對象,所以可以節約系統資源,對於一些需要頻繁建立和銷燬的對象。單例模式無疑可以提升系統的性能。

     3、贊成可變數目的實例。

主要缺點

     1、由於單利模式中沒有抽象層。所以單例類的擴展有很是大的困難。

     2、單例類的職責太重,在必定程度上違背了「單一職責原則」。

     3、濫用單例將帶來一些負面問題,如爲了節省資源將數據庫鏈接池對象設計爲單例類,可能會致使共享鏈接池對象的程序過多而出現鏈接池溢出;假設實例化的對象長時間不被利用。系統會以爲是垃圾而被回收,這將致使對象狀態的丟失。

  公司面試中。觀察者模式」也會被常常問到及寫出代碼,下篇博文將會分析解說。

參考資料

      1.http://www.iteye.com/topic/652440

      2.http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

美文美圖

 


相關文章
相關標籤/搜索