單例模式 2中建立方法

weixin_34119545發表於2015-12-12

1.java模式之單例模式

     單例模式確保一個類只有一個例項,自行提供這個例項並向整個系統提供這個例項。

      特點: 1,一個類只能有一個例項 2,自己建立這個例項 3,整個系統都要使用這個例項 Singleton模式主要作用是保證在Java應用程式中,一個類Class只有一個例項存在。在很多操作中,比如建立目錄 資料庫連線都需要這樣的單執行緒操作。一些資源管理器常常設計成單例模式。 外部資源:譬如每臺計算機可以有若干個印表機,但只能有一個Printer Spooler,以避免兩個列印作業同時輸出到印表機中。每臺計算機可以有若干個通訊埠,系統應當集中管理這些通訊埠,以避免一個通訊埠被兩個請求同時呼叫。

    有時會出現這樣一種需求:在系統執行時刻,某一個類在全域性範圍內只有一個物件例項,而且獲取該物件例項只能通過該物件類特定的訪問介面來獲取,以便繞過常規的構造器,來避免在全域性執行環境中例項化多個類物件例項。面對這樣的一種需求,如何提供一種封裝機制來保證一個物件類只有一個例項?需要注意的是,客戶端使用該物件類時,是不會考慮此類是否只有一個例項存在的問題,這應該屬於類設計者的責任,由其來保證,而不是類使用者的責任。同時,由於這個全域性唯一的物件例項擁有了所屬類的全部“權力”,自然它也就擔負起了行使這些權力的職責!這就是我們說的——單例模式!

2.意圖

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

結構圖

image

  1. Singleton(單例)角色:定義一個Instance操作,允許客戶訪問它的唯一例項。Instance是一個類操作,通常為靜態方法,負責建立它自己的唯一例項。

程式碼示例

 1:  public class Singleton {
 2:      static Singleton instance=null;
 3:      private Singleton(){
 4:   
 5:      }
 6:      public static Singleton Instance(){
 7:          if(instance==null){
 8:              instance=new Singleton();
 9:          }
10:          return instance;
11:      }
12:      //省略其他功能方法。。。
13:  }

   上述示例程式碼是單例模式最簡單的實現,實現了單例模式的基本要求,提供一個全域性訪問點來獲取得到單例類物件唯一物件例項,同時將單例類的建構函式訪問修飾符設定為private,阻止客戶程式直接通過new的方式來建立單例類的物件例項,保證物件的全域性唯一性。當然,這只是示例程式碼,在實際的生產環境中,我們需要根據執行緒安全、效能等方面來改造單例模式的實現方式,下方將會有詳細的講解。

現實場景

            在我們日常的現實生活中,有很多常見的場景與我們所講的單例模式很相似。比如,每臺計算機可以有很多印表機,但是隻能有一個Printer Spooler,避免兩個列印任務同時輸出到印表機中;國家主席職位是一個單例模式,因為我們國家憲法規定在任何一個時刻,國家只能有一個國家主席(不包括副主席),所以不管當前主席個人身份如何,我們總是可以通過中華人民共和國主席這個職稱來獲取當前國家主席的所有資訊,換句話來說,也只有通過它來行使憲法賦予它的各種權力和義務,因為它的”全域性唯一性“。再比如,就是我們日常web開發常用的spring開源框架中對各種bean建立時的物件例項個數的指定,即scope=”singleton”,這個配置項的目的便是告之spring容器,該bean在執行時刻只能存在一個全域性例項,也就是任何一個地方引用的都是這個唯一的例項物件,深入spring原始碼,我們也不能發現,其實其內部基本上也是單例模式的實現而已。

     接下來,我們著重來講述一下,對單例模式的不同實現方式及其特點吧。這也是單例模式中最有意義的部分呢,希望大家一起來學習、理解並掌握它們的不同之處。

    示例程式碼就是一種最簡單的單例實現方式,在單執行緒的環境之下,基本可以勝任,但是若是置於多執行緒的環境中,就面臨著執行緒安全問題呢。之所以這麼說,是因為可能會出現多個單例物件。下面我們通過圖示的方法來說明一下示例程式碼中獲取單例物件的Instance()方法是如何在執行時刻建立出兩個所謂的單例物件的,現在假設,有兩個對執行緒A和B,它們同時呼叫Instance()方法,那麼實際的執行過程就有可能是這樣的:

image

如果按照上圖的執行順序,那麼,這裡A執行緒和B執行緒就都各建立了一個單例例項物件,也就違反了單例模式的本質。示例程式碼中的單例模式實現方式為懶漢式,如果要求執行緒安全,通常有兩種方式,一個是在Instance()方法上加上sysnchronized關鍵字,即:

1:  public static synchronized Singleton Instance(){
2:      if(instance==null){
3:          instance=new Singleton();
4:      }
5:      return instance;
6:  }

     關鍵字synchronized將會保證Instance()方法的執行緒安全,但是這樣一來,會降低整個訪問速度,而且每次都需要進行判斷。有沒有一種更好的方式來實現懶漢式實現方式即執行緒安全又能保證執行效率呢?

    答案就是雙重加鎖機制,具體指的是:並不是每次進入Instance()方法都需要進行同步,而是先不同步,進入方法過後,先檢查例項是否存在,如果不存在才進入下面的同步塊,這是第一重檢票。進入同步塊後,再次檢查例項是否存在,如果不存在,就在同步的情況下建立一個單例物件例項,這是第二重檢查。這樣一來,就只需要同步一次,從而減小了同步情況下進行判斷所浪費的時間。

     雙重檢查加鎖機制的實現會使用一個關鍵字volatile,它的意思是:被volatile修飾的變數的值,將不會被本地執行緒快取,所有對該變數的讀寫都是直接操作共享記憶體,從而確保多個執行緒能正確的處理該變數。需要注意的是,在java1.4版本及之前版本中,很多JVM對volatile關鍵字的實現有問題,建議在java5及以上版本上使用。實現程式碼如下:

 1:  public class Singleton {
 2:      private static volatile Singleton instance=null;
 3:      private Singleton(){
 4:   
 5:      }
 6:      public static  Singleton Instance(){
 7:          if(instance==null){
 8:              synchronized (Singleton.class) {
 9:                  if(instance==null){
10:                      instance=new Singleton();
11:                  }
12:              }
13:          }
14:          return instance;
15:      }
16:  }

   這裡需要提及的一點是,由於volatile關鍵字可能會遮蔽掉虛擬機器中一些必要的程式碼優化,所以執行效率並不是很高。因此一般建議,沒有特別的需要,不建議使用。換句話來說,雖然使用雙重加鎖機制可以實現執行緒安全的單例模式,但並不建議大量使用。既然這樣子,是否有一種更加合理的方式來完成對單例模式的實現呢?

     這裡也有兩種選擇,一種是餓漢式實現方式,一種還是懶漢式實現方式

     餓漢式實現方式比較簡單,直接在Singleton類載入的過程中,就將靜態型別的instance變數例項化,由虛擬來保證其執行緒安全性,唯一不足的是無法實現延遲載入,不過這種方式,通過情況下已經足夠高效呢,實現也比較簡單,示例程式碼如下所示:

1:  public class Singleton {
2:      static Singleton instance=new Singleton();
3:      private Singleton(){}
4:   
5:      public static Singleton Instance(){
6:          return instance;
7:      }
8:  }

         不過也有一種懶漢式的實現方方式,名日:Lazy initialization holder class模式,其綜合使用了java的類級內部類和多執行緒預設同步鎖知識,很巧妙地同時實現了延遲載入和執行緒安全,在介紹其具體的實現之前,我們先來普及下基礎知識吧!

     何謂類級內部類,指的是有static修飾的成員式內部類,如果沒有static修飾的內部類稱為物件級內部類。類級內部類相當於其外部類的static成分,它與物件與外部類物件間不存在依賴關係,因此可以直接建立。在類級內部類中,可以定義靜態的方法,在靜態方法中只能引用外部類中的靜態方法或者靜態成員變數。類級內部類相當於外部類的成員,只有在第一次被使用時才會被載入。

接下來,我們看看有哪些情況下,JVM會隱含地為我們執行同步操作,這些情況下,我們不需要自己來進行同步控制呢:

  1. 由靜態初始化器(在靜態欄位上或者是static{}塊中的初始化器)初始化資料時。
  2. 訪問final欄位時。
  3. 在建立執行緒之前建立物件時。
  4. 執行緒可以看見它將要處理的物件時。

有了上面兩部分知識,再來理解這種高效的單例實現方式就比較簡單呢!

懶漢式:

 1:  public class Singleton {
 2:      //類級內部類,與外部類例項沒有繫結關係,只有在被呼叫時才會被載入,從而實現了延遲載入
 3:      private static class SingletonHolder{
 4:          //靜態初始化器,由JVM來保證執行緒安全
 5:          private static Singleton instance=new Singleton();
 6:      }
 7:   
 8:      private Singleton(){}
 9:   
10:      public static Singleton Instance(){
11:          return SingletonHolder.instance;
12:      }
13:  }

             結合上面的基礎知識和程式碼上的註釋,仔細想想,這種方法是不是很巧妙呢?在Instance()方法第一次被呼叫時,它第一次讀取SingletonHolder.instance,導致SingletonHolder類得到初始化;而SingletonHolder類被裝載並初始化的時候,也會初始化其靜態域,也就會建立Singleton的物件例項,因為是靜態域,因此會由在虛擬機器裝載類的時候初始化一次並由JVM來保證其執行緒安全性。綜上所述,該實現方法,不僅實現了延遲載入,又實現了執行緒安全,確實是一種值得推薦的單例實現方法,大家好好理解此方法的精妙之處吧!(來自《研磨設計模式》一書)

實現要點

  1. 單例模式模式用於限制對單例類例項的建立
  2. Singleton類的構造器可以是protected,被子類派生
  3. Singleton類一般不需要實現Cloneable介面,因為克隆操作可能會產生多個單例類物件,與單例模式的初衷相佐
  4. 單例模式關注點在單例類的例項的建立上,沒有涉及例項的銷燬管理等工作,我們可以通過使用一個單例登錄檔來完成對各種單例類例項的管理工作。

運用效果

  1. 物件例項控制:單例模式提供全域性訪問點,可以保證對客戶端都只訪問到唯一一個單例例項物件。
  2. 建立的方便性:由於單例類控制了例項建立,可以根據實際情況方便地修改單例物件的例項化過程。
  3. 由於單例模式不能通過new的方式直接建立單例物件,因此單例類的使用都必須事先知道該類為單例類,否則會因為看不到原始碼,而造成類的使用性差的印象。
  4. 由於單例類全域性只有一個物件例項,但是對其的引用卻可能不只一個,因為不能簡單地對這個特殊的物件例項進行銷燬操作,換句話來說就是不能輕易地手工地銷燬該物件。在存在記憶體管理的語言中,這個問題我們可以不能過多地關注這個問題,執行時會幫我們自動銷燬已經不存在引用的物件,但是對於c++語言而言,如果簡單地銷燬單例物件,有可能會造成“懸浮引用”問題。

4單例模式的2種方式:

餓漢式
class Singleton {
private static Singleton instance=new Singleton();
private Singleton(){}
static Singleton getInstance() {
return instance;
}
}
懶漢式
class Singleton {
private static Singleton instance=null;
private Singleton(){}
static Singleton getInstance() {
if(instance==null)
instance=new Singleton();
return instance;
}
}

相關文章