學點架構設計——單例模式

寫在前面

寫代碼久了不想只是做一個寫寫if else的初級碼農,隨著coding經驗的積累以及對這份職業(yè)的更高期望,越來越認識到被設計過的代碼是那么的優(yōu)美而性感。凡事皆有道,coding的道就是程序員前輩們發(fā)現和總結出來的各種模式,為了深刻地理解前人寫的優(yōu)秀的代碼,也為了在工作中寫出更具水平的更高質量的代碼來,有必要學一學設計模式。其實我在工作學習中很早就在學習和使用設計模式了,但一直沒有系統(tǒng)地學習總結,所以用過就忘掉了,對每種模式的內涵知之甚淺,慚愧?。‖F在我想系統(tǒng)地再學一學這些,并用文字記錄下自己所學的內容,以加深理解并方便以后查閱。

定義及特點

單例模式確保一個類只有一個實例,并提供一個全局訪問點。其類結構圖如下:

單例模式類圖

單例模式是對象創(chuàng)建型模式,要實現一個單例類需滿足以下幾個要素:
(1) 一個私有的構造方法
(2) 一個指向自己實例的私有的靜態(tài)引用
(3) 一個返回自己實例的公有的靜態(tài)方法
所以一個單例類應類似下面的偽代碼:

public class Singleton{
    private Singleton(){} //私有的構造方法
    private static Singleton singleton; //指向自己實例的私有靜態(tài)引用
    public static Singleton getInstance(){ //返回自己實例的公有靜態(tài)方法
        …… //創(chuàng)建本單例類的唯一實例,并賦值給私有靜態(tài)引用
        return singleton;
    }
}

另外,如果要考慮單例類的防攻擊,在實現時還需要做到防止反序列化、防止反射、防止克隆。

//readResolve method to preserve Singleton property
private Object readResolve() throws ObjectStreamException {
    return INSTANCE;
}

//防止反射調用后創(chuàng)建新的Singleton實例
private static boolean flag = false;  
private Singleton(){  
    synchronized(Singleton.class)  
    {  
        if(flag == false){  
            flag = !flag;  
        } else{  
            throw new RuntimeException("單例模式被侵犯!");  
        }  
    }  
}

//防止克隆產生新的實例
@Override
protected Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException("Cannot clone instance of this class");
}

因為單例類對象的生命周期是從實例被創(chuàng)建到應用程序結束,所以一個高質量的單例類還需要滿足以下特點:
√ 懶加載 —— 在需要時才創(chuàng)建單例類的實例
√ 線程安全 —— 應保證在多線程環(huán)境下訪問單例類不會創(chuàng)建多個實例
√ 高性能 —— 獲取單例對象的過程中應減少同步的消耗

適用場景

由于單例類保證一個類只有一個實例,并且由此表現出了一些其他優(yōu)點,使得單例模式是在開發(fā)中比較常用的一種設計模式。根據單例類的優(yōu)點,單例模式通常適用于以下場景的類創(chuàng)建:
● 需要頻繁實例化然后銷毀的對象
● 創(chuàng)建對象時耗時過多或者耗資源過多,但又經常用到的對象
● 有狀態(tài)的工具類對象
● 頻繁訪問數據庫或文件的對象
● 需要保證數據一致性的配置文件類或工具類對象

實現方式

根據單例模式的思想內容以及單例類能解決的問題,單例模式的實現方式有多種,不同的實現方式可能在單例對象的實例化時機、線程安全性和訪問性能方面有所不同。下面分別整理各種實現方式及其優(yōu)缺點。

餓漢式單例

public class Singleton {  
    private static Singleton instance=new Singleton();  
    private Singleton(){}
    public static Singleton getInstance(){
        return instance;  
    }
} 

這種單例實現方法之所以被稱為餓漢式是因為它利用JVM機制在單例類被加載時實例化。其實例化時機比較早,所以沒有實現懶加載(Lazy Loading),即使這個實例在應用的生命周期內沒有被使用到,也會因為已經被加載而占用一定的內存空間;另外這種實現方式也無法給單例的實例化過程傳入必須的參數。餓漢式的實現方式比較簡單,在單例類被加載到內存時就完成了實例化,避免了線程同步的問題;另外因為實例沒有被使用而造成的內存浪費問題可以忽略,餓漢式單例的實現方式是被推薦使用的。
餓漢式單例的實例化過程還有一種寫法,不過本質上也是利用JVM的類加載機制,只是語法的應用而已,如下:

    private static Singleton instance = null;  
    static {  
        instance = new Singleton();  
    }

懶漢式單例

懶漢式單例的實現目的就是為了達到單例類的懶加載,即在單例類第一次被使用到的時候實例化該單例類,對應到單例類實現的基本要素就是在返回單例類實例的公有靜態(tài)方法中去實例化單例對象。懶漢式單例的最直接最簡單的實現如下:

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

上述實現方式在實際項目中并不可取,因為它存在線程安全問題,當有多個線程去調用getInstance()方法獲取Singleton的實例時,有可能得到的不是同一個對象,即有可能每個線程訪問getInstance()方法時各自創(chuàng)建一個Singleton實例。要解決這種實現方式的線程安全問題,可以考慮加鎖進行同步。

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

上面的改進是在getInstance()方法上加鎖進行同步,這樣雖然解決了線程安全的問題,但又帶來了訪問效率低下的問題。每個線程在訪問getInstance()方法獲得類實例的時候,都要進行同步操作,而其實這個方法只需執(zhí)行一次實例化代碼就夠了,只要單例對象還存在就可以直接return給訪問者。對懶漢式單例進一步優(yōu)化得到了既能保證線程安全又能有較高的訪問效率的雙重檢查鎖(Double Checked Locking)實現。

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

雙重檢查鎖的實現方式是比較健壯的懶漢式單例實現,有兩個問題需要理解:
1、雙重檢查有什么意義
雙重檢查就是在getInstance()方法中有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。為什么在同步塊內還要再檢查一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內不進行二次檢查的話就會生成多個實例了。
2、靜態(tài)引用instance前的volatile關鍵字有何作用
主要在于instance = new Singleton()這句并非是一個原子操作,在JVM中這句話大概做了下面 3 件事情:
① 給 instance 分配內存
② 調用 Singleton 的構造函數來初始化成員變量
③ 將instance對象指向分配的內存空間(執(zhí)行完這步 instance 就為非 null 了)
但是在JVM的即時編譯器中存在指令重排序的優(yōu)化,也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 ①-②-③ 也可能是 ①-③-②。如果是后者,則在 ③ 執(zhí)行完畢、② 未執(zhí)行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,然后就會報錯。
在Java1.5以后的版本中volatile修飾符具有禁止指令重排序優(yōu)化的特性,也就是說在 volatile 變量的賦值操作后面會有一個內存屏障(生成的匯編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執(zhí)行完 ①-②-③ 或者 ①-③-② 之后,不存在執(zhí)行到 ①-③ 然后取到值的情況。從「先行發(fā)生原則」的角度理解的話,就是對于一個 volatile 變量的寫操作都先行發(fā)生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。

靜態(tài)內部類實現單例

public class Singleton{
    private Singleton() {}
    private static class SingletonHolder{
        private static Singleton instance=new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

通過在類中創(chuàng)建一個靜態(tài)內部類來實現單例模式也是利用了JVM的類加載機制保證只創(chuàng)建一份實例,同時與餓漢式單例一樣具有線程安全性,而且客戶在獲取這個單例類實例的時候不會進行同步,沒有性能缺陷,也不依賴 JDK 版本。靜態(tài)內部類方式在Singleton類被裝載時并不會立即實例化,而是在需要實例化時調getInstance()方法,才會裝載SingletonHolder類實例化Singleton,這樣就實現了單例類的懶加載。
由于使用靜態(tài)內部類實現單例避免了線程不安全的問題,并且有較高的訪問效率以及實現了延遲加載,這種方式是值得推薦的單例實現方式,當然這種方式與餓漢式單例一樣不能傳入參數。

枚舉實現單例

public enum Singleton {
    INSTANCE;
    private Singleton() {}
    public void method() {
    }
 }

從Java1.5版本起,創(chuàng)建單例可以使用簡潔的枚舉類型。我們直接通過Singleton.INSTANCE來訪問實例,創(chuàng)建枚舉默認就是線程安全的,所以不需要考慮double checked locking,而且枚舉還能防止反序列化或者反射攻擊導致創(chuàng)建新的實例?!禘ffective Java》中更是說明單元素的枚舉類型已經成為實現Singleton的最佳方法。

單例模式小結

單例模式是類結構和模式思想最簡單的一種設計模式,也是實際項目在被使用最多的一種模式,Android SDK的許多類以及許多第三方開源庫都提供了很多有單例行為的類。單例模式的實現方式有多種,但在實際使用時需要認真考量單例類的線程安全性、訪問效率和懶加載屬性,所有的限制和設計都要保證Singleton類僅僅被實例化一次。

Thanks To:

如何正確地寫出單例模式
Java設計模式—單例設計模式(Singleton Pattern)完全解析
單例模式討論篇:單例模式與垃圾回收

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容