1. 程式人生 > >單例模式——靜態內部類單例原理

單例模式——靜態內部類單例原理

瞭解下單例的四大原則:

1.構造私有。
2.以靜態方法或者列舉返回例項。

3.確保例項只有一個,尤其是多執行緒環境。

4.確保反序列化時不會重新構建物件。

 

我們常用的單例模式有:

餓漢模式、懶漢模式、雙重鎖懶漢模式、靜態內部類模式、列舉模式,我們來逐一分析下這些模式的區別。

1.餓漢模式:

public class SingleTon{
    private static SingleTon INSTANCE = new SingleTon();
   private SingleTon(){}
   public static SingleTon getInstance(){ return
INSTANCE; }
}

餓漢模式在類被初始化時就已經在記憶體中建立了物件,以空間換時間,故不存線上程安全問題。

2.懶漢模式:

public class SingleTon{
   private static SingleTon  INSTANCE = null;
   private SingleTon(){}
   public static SingleTon getInstance() {  
   if(INSTANCE == null){
      INSTANCE = new SingleTon(); 
    } 
  }
}

懶漢模式在方法被呼叫後才建立物件,以時間換空間,在多執行緒環境下存在風險。

 

3.雙重鎖懶漢模式(Double Check Lock)

public class SingleTon{
  private static SingleTon  INSTANCE = null;
  private SingleTon(){}
  public static SingleTon getInstance(){
    if(INSTANCE == null){
       synchronized(SingleTon.class){
         if(INSTANCE == null){ 
        INSTANCE = new SingleTon();
       } 
     } 
    } 
  }
}

DCL模式的優點就是,只有在物件需要被使用時才建立,第一次判斷 INSTANCE == null為了避免非必要加鎖,當第一次載入時才對例項進行加鎖再例項化。這樣既可以節約記憶體空間,又可以保證執行緒安全。但是,由於jvm存在亂序執行功能,DCL也會出現執行緒不安全的情況。具體分析如下:

INSTANCE  = new SingleTon(); 

這個步驟,其實在jvm裡面的執行分為三步:

  1.在堆記憶體開闢記憶體空間。
  2.在堆記憶體中例項化SingleTon裡面的各個引數。
  3.把物件指向堆記憶體空間。

由於jvm存在亂序執行功能,所以可能在2還沒執行時就先執行了3,如果此時再被切換到執行緒B上,由於執行了3,INSTANCE 已經非空了,會被直接拿出來用,這樣的話,就會出現異常。這個就是著名的DCL失效問題。

不過在JDK1.5之後,官方也發現了這個問題,故而具體化了volatile,即在JDK1.6及以後,只要定義為private volatile static SingleTon  INSTANCE = null;就可解決DCL失效問題。volatile確保INSTANCE每次均在主記憶體中讀取,這樣雖然會犧牲一點效率,但也無傷大雅。

3.靜態內部類模式:

public class SingleTon{
  private SingleTon(){}
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

靜態內部類的優點是:外部類載入時並不需要立即載入內部類,內部類不被載入則不去初始化INSTANCE,故而不佔記憶體。即當SingleTon第一次被載入時,並不需要去載入SingleTonHoler,只有當getInstance()方法第一次被呼叫時,才會去初始化INSTANCE,第一次呼叫getInstance()方法會導致虛擬機器載入SingleTonHoler類,這種方法不僅能確保執行緒安全,也能保證單例的唯一性,同時也延遲了單例的例項化。
---------------------

那麼,靜態內部類又是如何實現執行緒安全的呢?首先,我們先了解下類的載入時機。


類載入時機:JAVA虛擬機器在有且僅有的5種場景下會對類進行初始化。
1.遇到new、getstatic、setstatic或者invikestatic這4個位元組碼指令時,對應的java程式碼場景為:new一個關鍵字或者一個例項化物件時、讀取或設定一個靜態欄位時(final修飾、已在編譯期把結果放入常量池的除外)、呼叫一個類的靜態方法時。
2.使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒進行初始化,需要先呼叫其初始化方法進行初始化。
3.當初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化。
4.當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的類),虛擬機器會先初始化這個類。
5.當使用JDK 1.7等動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。
這5種情況被稱為是類的主動引用,注意,這裡《虛擬機器規範》中使用的限定詞是"有且僅有",那麼,除此之外的所有引用類都不會對類進行初始化,稱為被動引用。靜態內部類就屬於被動引用的行列。

我們再回頭看下getInstance()方法,呼叫的是SingleTonHoler.INSTANCE,取的是SingleTonHoler裡的INSTANCE物件,跟上面那個DCL方法不同的是,getInstance()方法並沒有多次去new物件,故不管多少個執行緒去呼叫getInstance()方法,取的都是同一個INSTANCE物件,而不用去重新建立。當getInstance()方法被呼叫時,SingleTonHoler才在SingleTon的執行時常量池裡,把符號引用替換為直接引用,這時靜態物件INSTANCE也真正被建立,然後再被getInstance()方法返回出去,這點同餓漢模式。那麼INSTANCE在建立過程中又是如何保證執行緒安全的呢?在《深入理解JAVA虛擬機器》中,有這麼一句話:

 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個程序阻塞(需要注意的是,其他執行緒雖然會被阻塞,但如果執行<clinit>()方法後,其他執行緒喚醒之後不會再次進入<clinit>()方法。同一個載入器下,一個型別只會初始化一次。),在實際應用中,這種阻塞往往是很隱蔽的。

故而,可以看出INSTANCE在建立過程中是執行緒安全的,所以說靜態內部類形式的單例可保證執行緒安全,也能保證單例的唯一性,同時也延遲了單例的例項化。

那麼,是不是可以說靜態內部類單例就是最完美的單例模式了呢?其實不然,靜態內部類也有著一個致命的缺點,就是傳參的問題,由於是靜態內部類的形式去建立單例的,故外部無法傳遞引數進去,例如Context這種引數,所以,我們建立單例時,可以在靜態內部類與DCL模式裡自己斟酌。

 

利用了classloader的機制來保證初始化instance時只有一個執行緒,所以也是執行緒安全的,同時沒有效能損耗

 

優勢:

  • 兼顧了懶漢模式的記憶體優化(使用時才初始化)以及餓漢模式的安全性(不會被反射入侵)。

  劣勢:

  • 需要兩個類去做到這一點,雖然不會建立靜態內部類的物件,但是其 Class 物件還是會被建立,而且是屬於永久帶的物件。
  • 建立的單例,一旦在後期被銷燬,不能重新建立。


---------------------

最後粗略的介紹下列舉型別的單例吧。

列舉單例:

public enum SingleTon{
  INSTANCE;
        public void method(){
        //TODO
     }
}

列舉在java中與普通類一樣,都能擁有欄位與方法,而且列舉例項建立是執行緒安全的,在任何情況下,它都是一個單例。我們可直接以

SingleTon.INSTANCE