設計模式1---單例模式(Singleton pattern)

單例模式Singleton

面試的時候,問到許多年輕的Android開發他所會的設計模式是什麼,基本上都會提到單例模式,但是對

單例模式也是一知半解,在Android開發中我們經常會運用單例模式,所以我們還是要更瞭解單例模式纔對。

定義:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
單例模式結構圖:
在這裏插入圖片描述
單例模式有多種寫法各有利弊,現在我們來看看各種模式寫法。

  1. 餓漢模式
public class Singleton {  
     private static final Singleton instance = new Singleton();       

   private Singleton (){
     }

     public static Singleton getInstance() {  
       return instance;  
     }  
 }

這種方式在類加載時就完成了初始化,所以類加載較慢,但獲取對象的速度快。 這種方式基於類加載機制避免了多線程的同步問題,

但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到懶加載的效果。

2.懶漢模式(線程不安全)

public class Singleton {  
      private static Singleton instance;  

      private Singleton (){
      }   

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

懶漢模式申明瞭一個靜態對象,在用戶第一次調用時初始化,雖然節約了資源,但第一次加載時需要實例化,

反映稍慢一些,而且在多線程不能正常工作。

  1. 懶漢模式(線程安全)
public class Singleton {  
      private static Singleton instance;  

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

 }

這種寫法能夠在多線程中很好的工作,但是每次調用getInstance方法時都需要進行同步,造成不必要的同步開銷,

而且大部分時候我們是用不到同步的,所以不建議用這種模式。

  1. 雙重檢查模式 (DCL)
public class Singleton {  
      private volatile static Singleton singleton;  

      private Singleton(){
      }   
      public static Singleton getInstance() {  
          if (instance== null) {  
              synchronized (Singleton.class) {  
                  if (instance== null) {  
                        instance= new Singleton();  
                  }  
             }  
         }  
         return singleton;  
     } 
 } 

這種寫法在getSingleton方法中對singleton進行了兩次判空,第一次是爲了不必要的同步,第二次是在singleton等於null的情況下才創建實例。在這裏用到了volatile關鍵字,不瞭解volatile關鍵

字的可以查看Java多線程(三)volatile域這篇文章,在這篇文章我也提到了雙重檢查模式是正確使用volatile關鍵字的場景之一。
在這裏使用volatile會或多或少的影響性能,但考慮到程序的正確性,犧牲這點性能還是值得的。 DCL優點是資源利用率高,第一次執行getInstance時單例對象才被實例化,效率高。缺點是第一次加載時反應稍慢一些,在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL雖然在一定程度解決了資源的消耗和多餘的同步,線程安全等問題,但是他還是在某些情況會出現失效的問題,也就是DCL失效,在《java併發編程實踐》一書建議用靜態內部類單例模式來替代DCL。

  1. 靜態內部類單例模式
public class Singleton { 
    private Singleton(){
    }

    public static Singleton getInstance(){  
        return SingletonHolder.sInstance;  
    }  

    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    } 
 
}

第一次加載Singleton類時並不會初始化sInstance,只有第一次調用getInstance方法時虛擬機加載

SingletonHolder 並初始化sInstance ,這樣不僅能確保線程安全也能保證Singleton類的唯一性,所以

推薦使用靜態內部類單例模式。

  1. 枚舉單例
public enum Singleton {  
     INSTANCE;  

     public void doSomeThing() {  
     }  

 }

默認枚舉實例的創建是線程安全的,並且在任何情況下都是單例,上述講的幾種單例模式實現中,有一種情況下他們會重新創建對象,那就是反序列化,將一個單例實例對象寫到磁盤再讀回來,從而獲得了一個實例。反序列化操作提供了readResolve方法,這個方法可以讓開發人員控制對象的反序列化。在上述的幾個方法示例中如果要杜絕單例對象被反序列化是重新生成對象,就必須加入如下方法:

private Object readResolve() throws ObjectStreamException{
    return singleton;
}

枚舉單例的優點就是簡單,但是大部分應用開發很少用枚舉,可讀性並不是很高,不建議用。

  1. 使用容器實現單例模式
public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  
     private Singleton() { 
  }

  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {   
           objMap.put(key, instance) ;
    }
  }

  public static Object getService(String key) {
    return objMap.get(key) ;
  }

} 

用SingletonManager 將多種的單例類統一管理,在使用時根據key獲取對象對應類型的對象。這種方式

使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使

用成本,也對用戶隱藏了具體實現,降低了耦合度。

總結
到這裏七中寫法都介紹完了,至於選擇用哪種形式的單例模式,取決於你的項目本身,是否

是有複雜的併發環境,還是需要控制單例對象的資源消耗。

基於volatile的雙重檢查鎖(double-checked locking)

public class Singleton {
    //private static Singleton instance = null;
    //防止DCL失效問題,保證instance對象每次都是從住內存中讀取
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {            
        if (instance == null) { //避免不必要的同步
            synchronized (Singleton.class) {
                //在instance = null下創建實例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

缺點:
JDK1.5以上版本纔有volatile。
優點:
資源利用率高,第一次執行getInstance()時纔會被實例化,效率高。只是看起來很完美這種看起來很完美的優化技巧就是double-checked locking,但遺憾地告訴你,根據JLS規範,上面的代碼不可靠!線程有可能得到一個不爲null,但是構造不完全的對象。Why?造成不可靠的原因是編譯器爲了提高執行效率的指令重排。只要認爲在單線程下是沒問題的,它就可以進行亂序寫入!以保證不要讓cpu指令流水線中斷。
指令重排
爲了提高代碼的執行效率,JVM會將執行頻率高的代碼編譯成機器碼;而對於頻率不高的代碼則仍然採用解釋執行。
常見的編譯優化方式有:
方法內聯:免去參數、返回值傳遞過程
去虛擬化:接口的方法只有一個實現類,進行方法內聯
冗餘消除:運行時去掉無用代碼
還有一些編譯優化根據“逃逸分析”技術
標量替換:User u=new User(“zhang3”,18)
String n=“zhang3” int age=18,節省內存
棧上分配:逃逸對象直接在棧上分配,快速,GC及時
同步削除:去掉不必要的同步
new Instance()到底發生了什麼?
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
上面的僞代碼中2、3步可能重排

解決方案1
Java5以後的版本,可以利用volatile關鍵字。
Why?
在java5以前,volatile原語不怎麼強大,只能保證對象的可見性,但在java5之後,volatile語義加強了,被volatile修飾的對象,將禁止該對象上的讀寫指令重排序
這樣,就保證了線程B讀對象時,已經初始化完全了

解決方案2

這也是官方比較推薦的一種方案(effective java 2nd)
點擊(此處)摺疊或打開

public class InstanceHolder{
    private Instance(){}

    //Lazy initialization holder class idiom for static fields
    private static class Inner{
         private static final Instance ins = new Instance()
    }

    public static Instance getInstance(){
        return Inner.ins;
     }    
}

原理:一個類只有在被使用時纔會初始化,而類初始化過程是非並行的,這些都由JLS能保證。

靜態內部類單例模式
在《Java 併發編程實踐》中提及了DCL失效的問題(上面基於volatile的雙重檢查鎖例子中註釋的部分),建議使用如下代碼替換:

public class Singleton {
    private static final Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.sInstance;

    }

    private static class SingletonHolder {
         sInstance = new Singleton();
    }

}

優點:
資源利用率高,第一次執行getInstance()時纔會被實例化,效率高。
不僅保證線程安全,而且也保證了單例對象的唯一性,同時也延遲了單例的實例化,所以這纔是推薦使用的單例模式實現方式。
枚舉單例

public class SingletonEnum {
    INSTANCE;
    
    public void doSomething() {
        //do something;
    }

}

優點:

  • 枚舉在Java中於普通的類一樣,可以有字段,方法。 默認枚舉創建的實例是線程安全的。
  • 使用enum實現的單例自帶防序列化。

然而上述的幾種單例模式實現中在反序列化時會重新創建對象。要杜絕單例對象重新生成對象,必須要加入如下方法:

private Object readResolve() throws ObjectStreamException {
    return sInstance;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章