設計模式--單例模式幾種寫法及比較


在我們日常的工作中經常需要在應用程序中保持一個唯一的實例,如:IO處理,數據庫操作等,由於這些對象都要占用重要的系統資源,所以我們必須限制這些實例的創建或始終使用一個公用的實例,這就是我們今天要介紹的——單例模式(Singleton)。

定義

單例模式,又稱單件模式或者單子模式,指的是一個類只有一個實例,並且提供一個全局訪問點。

實現思路

在單例的類中設置一個 private 靜態變量instance,instance 類型為當前類,用來持有單例唯一的實例。
將(無參數)構造器設置為 private,避免外部使用 new 構造多個實例。
提供一個 public 的靜態方法,如 getInstance,用來返回該類的唯一實例 instance。

類圖

幾種實現方式

由於使用場景不同,出現不同寫法和模式,它們分別:

  • 懶漢式

  • 惡漢式

  • 雙重校驗鎖

  • 枚舉

  • 靜態內部類

由於枚舉使用場景場景較少, 下面就不介紹,感興趣的可以自行解決。

惡漢式

餓漢式指的是單例的實例在類裝載時進行創建。由於是在類裝時候創建, 所以能夠保證線程安全。如果單例類的構造方法中沒有包含過多的操作處理,餓漢式其實是可以接受的。

  1. public class SingleInstance {

  2.  private final static SingleInstance instance = new SingleInstance();

  3.  

  4.  public static SingleInstance getInstance() {

  5.      return instance;

  6.  }

  7. }

不足:

  • 如果構造方法中存在過多的處理,會導致加載這個類時比較慢,可能引起性能問題。

  • 如果使用餓漢式的話,只進行了類的裝載,並沒有實質的調用,會造成資源的浪費。

懶漢式

懶漢式指的是單例實例在第一次使用時進行創建。這種情況下避免了上面餓漢式可能遇到的問題。

 

public class SingleInstance {

  1.  private static SingleInstance instance;

  2.  private SingleInstance() {

  3.  }

  4.  

  5.  public static SingleInstance getInstance() {

  6.      if (null == instance) {

  7.          instance = new SingleInstance();

  8.      }

  9.      return instance;

  10.  }

  11. }

但是如果上面的代碼在多線程並發的情況下就會發生問題, 因為它們存在共同的「臨界資源」 instance, 比如線程A進入 null == instance 這段代碼塊,而在A線程未創建完成實例時,這時線程B也進入了該代碼塊,必然會造成兩個實例的產生。

所以如果多線程這里要考慮加鎖同步。代碼實現如下:

  1. public class SingleInstance {

  2.  private static SingleInstance instance;

  3.  private SingleInstance() {

  4.  }

  5.  

  6.  public static synchronized SingleInstance getInstance() {

  7.      if (null == instance) {

  8.          instance = new SingleInstance();

  9.      }

  10.      return instance;

  11.  }

  12. }

如果使用 synchronized 修飾 getInstance 方法后必然會導致性能下降,而且 getInstance 是一個被頻繁調用的方法。雖然這種方法能解決問題,但是不推薦使用在多線程的情況下。所以偉大人類又想到了 「雙重檢查加鎖」。

雙重校驗鎖

偉大人類想到首先進入該方法時進行 null == sInstance 檢查,如果第一次檢查通過,即沒有實例創建,則進入 synchronized 控制的同步塊,並再次檢查實例是否創建,如果仍未創建,則創建該實例。

  1. public class SingleInstance {

  2.  private static SingleInstance instance;

  3.  private SingleInstance() {

  4.  }

  5.  

  6.  public static SingleInstance getInstance() {

  7.      if (null == instance) {

  8.          synchronized (SingleInstance.class) {

  9.              if(null == instance) {

  10.                  instance = new SingleInstance();

  11.              }

  12.          }

  13.      }

  14.      return instance;

  15.  }

  16. }

雙重檢查加鎖保證了多線程下只創建一個實例,並且加鎖代碼塊只在實例創建的之前進行同步。如果實例已經創建后,進入該方法,則不會執行到同步塊的代碼。

靜態內部類

  1. public class SingleInstance {

  2.  private SingleInstance() {

  3.  }

  4.  

  5.  public static SingleInstance getInstance() {

  6.      return SingleInstanceHolder.sInstance;

  7.  }

  8.  

  9.  private static class SingleInstanceHolder {

  10.      private static SingleInstance sInstance = new SingleInstance();

  11.  }

  12. }

上面的代碼 Singleton 類被裝載了,instance 不一定被初始化。因為 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,才會顯示裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,我想讓它延遲加載, 上面就能這種方式就能達到。

優點:

  • 單例模式(Singleton)會控制其實例對象的數量,從而確保訪問對象的唯一性。

  • 實例控制:單例模式防止其它對象對自己的實例化,確保所有的對象都訪問一個實例。

  • 伸縮性:因為由類自己來控制實例化進程,類就在改變實例化進程上有相應的伸縮性。

缺點:

  • 系統開銷。雖然這個系統開銷看起來很小,但是每次引用這個類實例的時候都要進行實例是否存在的檢查。這個問題可以通過靜態實例來解決。

  • 使用多個類加載器加載單例類,也會導致創建多個實例並存的問題。

  • 使用反射,雖然構造器為非公開,但是在反射面前就不起作用了。

  • 對象生命周期。因為單例模式沒有提出對象的銷毀, 所以使用時容易造成內存泄漏, 例如在 Android 中在 Activity 中使用單例, 所以我們要額外小心。

使用場景

  • 系統只需要一個實例對象,如系統要求提供一個唯一的序列號生成器,或者需要考慮資源消耗太大而只允許創建一個對象。

  • 不要使用單例模式存取全局變量。這違背了單例模式的用意,最好放到對應類的靜態成員中。

  • 在一個系統中要求一個類只有一個實例時才應當使用單例模式。反過來,如果一個類可以有幾個實例共存,就需要對單例模式進行改進,使之成為多例模式

Android 系統中的應用

在 Android 系統中, 大量使用單例模式, 我們來看一下。

惡漢式:

  1. public class CallManager {

  2.    ...

  3.    // Singleton instance

  4.    private static final CallManager INSTANCE = new CallManager();

  5.  

  6.    public static CallManager getInstance() {

  7.        return INSTANCE;

  8.    }

  9.    ....

  10. }

懶漢式非線程安全實現方式:

  1. class SnackbarManager {

  2.    .....

  3.    private static SnackbarManager sSnackbarManager;

  4.  

  5.    static SnackbarManager getInstance() {

  6.        if (sSnackbarManager == null) {

  7.            sSnackbarManager = new SnackbarManager();

  8.        }

  9.        return sSnackbarManager;

  10.    }

  11. }

懶漢式線程安全實現方式:

  1. public class SystemConfig {

  2.    ...

  3.    static SystemConfig sInstance;

  4.    ...

  5.    public static SystemConfig getInstance() {

  6.        synchronized (SystemConfig.class) {

  7.            if (sInstance == null) {

  8.                sInstance = new SystemConfig();

  9.            }

  10.            return sInstance;

  11.        }

  12.    }

  13. }

總結

一般的情況下,構造方法沒有太多處理時,我會使用「惡漢」方式, 因為它簡單易懂,而且在JVM層實現了線程安全(如果不是多個類加載器環境)。只有在要明確實現延遲加載效果時我才會使用「靜態內部類」方式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM