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

Glearn發表於2018-01-31

1. 概述

In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one object. This is useful when exactly one object is needed to coordinate actions across the system. The concept is sometimes generalized to systems that operate more efficiently when only one object exists, or that restrict the instantiation to a certain number of objects. The term comes from the mathematical concept of a singleton. - wikipedia

單例模式:是一種物件建立型模式,用來確保程式中一個類最多隻有一個例項,並提供訪問這個例項的全域性點

單例模式解決以下類似問題:

  • 如何樣保證一個類只有一個例項?
  • 如何輕鬆地訪問類的唯一例項?
  • 一個類如何控制它的例項化?
  • 如何限制一個類的例項數量?

單例模式的解決方案:

  • 隱藏類的構造方法,用private修飾符修飾構造方法.
  • 定義一個public的靜態方法(getInstance()) ,返回這個類唯一靜態例項。

2. 適用場景

以下場景可使用單例模式:

  • 某些管理類,保證資源的一致訪問性。
  • 建立物件時耗時過多或耗費資源過多,但又經常用到的物件;
  • 工具類物件;
  • 頻繁訪問資料庫或檔案的物件。

Android:

  • Context.getSystemService()

  • KeyguardUpdateMonitor.getInstance(mContext)

  • Calendar.getInstance()

  • ....

其他:

  • 日誌操作類
  • 檔案管理器
  • 資料庫的連線尺
  • ...

3.實現方式

3.1 餓漢式

餓漢式,故名思議,很餓,所以在類載入的時候就直接建立類的例項。

/**
 * Singleton class. Eagerly initialized static instance guarantees thread safety.
 */
public final class Singleton {

  /**
   * Private constructor so nobody can instantiate the class.
   */
  private Singleton() {}

  /**
   * Static to class instance of the class.
   */
  private static final Singleton INSTANCE = new Singleton();

  /**
   * To be called by user to obtain instance of the class.
   *
   * @return instance of the singleton.
   */
  public static Singleton getInstance() {
    return INSTANCE;
  }
}
複製程式碼

優點:

  • 多執行緒安全

缺點:

  • 記憶體浪費,類載入之後就被建立了例項,但是如果某次的程式執行沒有用到,記憶體就被浪費了。

小結:

  • 適合:單例佔用記憶體比較小,初始化時就會被用到的情況。

  • 不適合:單例佔用的記憶體比較大,或單例只是在某個特定場景下才會用到

3.2 懶漢式

​ 懶漢式,故名思議,很懶,需要用的時候才建立例項。

/**
 * Singleton class. Eagerly initialized static instance guarantees thread safety.
 */
public final class Singleton {

  /**
   * Private constructor so nobody can instantiate the class.
   */
  private Singleton() {}

  /**
   * Static to class instance of the class.
   */
  private static final Singleton INSTANCE = null;

  /**
   * To be called by user to obtain instance of the class.
   *
   * @return instance of the singleton.
   */
  public static Singleton getInstance() {
    if (INSTANCE == null){
        INSTANCE = new Singleton();
    }
    return INSTANCE;
  }
}
複製程式碼

優點:

  • 記憶體節省,由於此種模式的例項實在需要時建立,如果某次的程式執行沒有用到,就是可以節省記憶體

缺點:

  • 執行緒不安全,分析如下
    執行緒1 執行緒2 INSTANCE
    public static Singleton getInstance() { null
    public static Singleton getInstance() { null
    if (INSTANCE == null){ null
    if (INSTANCE == null){ null
    INSTANCE = new Singleton(); object1
    return INSTANCE; object1
    INSTANCE = new Singleton(); object2
    return INSTANCE; object2
    糟糕的事發生了,這裡返回2個不同的例項。

小結:

  • 適合:單執行緒,記憶體敏感的

  • 不適合:多執行緒

3.3 執行緒安全的懶漢式

/**
 * Thread-safe Singleton class. The instance is lazily initialized and thus needs synchronization
 * mechanism.
 *
 * Note: if created by reflection then a singleton will not be created but multiple options in the
 * same classloader
 */
public final class ThreadSafeLazyLoadedSingleton {

  private static ThreadSafeLazyLoadedSingleton instance;

  private ThreadSafeLazyLoadedSingleton() {
  // to prevent instantiating by Reflection call
    if (instance != null) {
        throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * The instance gets created only when it is called for first time. Lazy-loading
   */
  public static synchronized ThreadSafeLazyLoadedSingleton getInstance() {
    if (instance == null) {
        instance = new ThreadSafeLazyLoadedSingleton();
    }
    return instance;
  }
}
複製程式碼

優點:

  • 多執行緒安全

缺點:

  • 執行效率低,每個執行緒在想獲得類的例項時候,執行getInstance()方法都要進行同步。而其實這個方法只執行一次例項化程式碼就夠了,後面的想獲得該類例項,直接return就行了。方法進行同步效率太低要改進。

小結:

  • 不建議使用此方法,後續介紹其他方法可兼顧記憶體和多執行緒安全.

3.4 執行緒安全的雙重檢查

/**
 * Double check locking
 * <p/>
 * http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html
 * <p/>
 * Broken under Java 1.4.
 *
 * @author mortezaadi@gmail.com
 */
public final class ThreadSafeDoubleCheckLocking {

  private static volatile ThreadSafeDoubleCheckLocking instance;

  /**
   * private constructor to prevent client from instantiating.
   */
  private ThreadSafeDoubleCheckLocking() {
    // to prevent instantiating by Reflection call
    if (instance != null) {
      throw new IllegalStateException("Already initialized.");
    }
  }

  /**
   * Public accessor.
   *
   * @return an instance of the class.
   */
  public static ThreadSafeDoubleCheckLocking getInstance() {
    // local variable increases performance by 25 percent
    // Joshua Bloch "Effective Java, Second Edition", p. 283-284
    
    ThreadSafeDoubleCheckLocking result = instance;
    // Check if singleton instance is initialized. If it is initialized then we can return the       // instance.
    if (result == null) {
      // It is not initialized but we cannot be sure because some other thread might have             // initialized it
      // in the meanwhile. So to make sure we need to lock on an object to get mutual exclusion.
      synchronized (ThreadSafeDoubleCheckLocking.class) {
        // Again assign the instance to local variable to check if it was initialized by some           // other thread
        // while current thread was blocked to enter the locked zone. If it was initialized then         // we can 
        // return the previously created instance just like the previous null check.
        result = instance;
        if (result == null) {
          // The instance is still not initialized so we can safely (no other thread can enter             // this zone)
          // create an instance and make it our singleton instance.
          instance = result = new ThreadSafeDoubleCheckLocking();
        }
      }
    }
    return result;
  }
}
複製程式碼

優點:

  • 多執行緒安全

注意點:

  • jdk 1.5以下多執行緒安全不能實現

小結:

  • 可使用此方法,兼顧記憶體和多執行緒安全.

3.5 靜態內部類

public class Singleton {
    private Singleton() {
    }

    /**
     * 類級的內部類,也就是靜態的成員式內部類,該內部類的例項與外部類的例項
     * 沒有繫結關係,而且只有被呼叫到時才會裝載,從而實現了延遲載入。
     */
    public static Singleton getInstance() {
        return SingletonLazyHolder.instance;
    }

    private static class SingletonLazyHolder {
        /**
         * 靜態初始化器,由JVM來保證執行緒安全
         */
        private final static Singleton instance = new Singleton();
    }
}
複製程式碼

優點:

  • 多執行緒安全

注意點:

  • jdk 1.5以下多執行緒安全不能實現

小結:

  • 可使用此方法,兼顧記憶體和多執行緒安全.

3.6 列舉

public enum EnumSingleton {

  INSTANCE;

  @Override
  public String toString() {
    return getDeclaringClass().getCanonicalName() + "@" + hashCode();
  }
}
複製程式碼

小結:

  • 可使用此方法,兼顧記憶體和多執行緒安全.同時這個也是Effective Java推薦使用的方法。注意列舉也是jdk 1.5開始加入的。

4.總結

單例佔用記憶體比較小,初始化時就會被用到的情況 - 推薦使用方法 3.1

多執行緒安全和記憶體佔用大,特定場景下采用,推薦使用方法 3.4.3.5,3.6. 使用時注意jdk的版本。個人推薦使用 3.4.3.5

5.Android程式碼例項

5.1 Dialer,使用方法3.2

packages/apps/Dialer/java/com/android/incallui/InCallPresenter.java

private static InCallPresenter sInCallPresenter;
/** Inaccessible constructor. Must use getRunningInstance() to get this singleton. */
@VisibleForTesting
InCallPresenter() {}
public static synchronized InCallPresenter getInstance() {
    if (sInCallPresenter == null) {
      sInCallPresenter = new InCallPresenter();
    }
    return sInCallPresenter;
}

//其他無關程式碼省略
複製程式碼

5.2 Email,使用方法3.5

packages/apps/Dialer/java/com/android/incallui/InCallPresenter.java

public class NotificationControllerCreatorHolder {
    private static NotificationControllerCreator sCreator =
            new NotificationControllerCreator() {
                @Override
                public NotificationController getInstance(Context context){
                    return null;
                }
            };

    public static void setNotificationControllerCreator(
            NotificationControllerCreator creator) {
        sCreator = creator;
    }

    public static NotificationControllerCreator getNotificationControllerCreator() {
        return sCreator;
    }

    public static NotificationController getInstance(Context context) {
        return getNotificationControllerCreator().getInstance(context);
    }
}
複製程式碼

有興趣的可以自己再找找案例看看。

6.有引數的單例

android上有很多需要Context引數的單例場景。先不要急,看看Android原始碼的例項:

packages/apps/Email/src/com/android/email/EmailNotificationController.java

    private static EmailNotificationController sInstance;
    /** Singleton access */
    public static synchronized EmailNotificationController getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new EmailNotificationController(context, Clock.INSTANCE);
        }
        return sInstance;
    }
複製程式碼

其實也很簡單嗎,但是這裡面有個小問題,如果傳遞引數是敏感的,是需要替換的,那就需要在處理一下:

public final class Singleton {

    private Context context;
    private static volatile Singleton instance;

    private Singleton(Context context) {
        this.context = context;
        if (instance != null) {
            throw new IllegalStateException("Already initialized.");
        }
    }

    public static Singleton getInstance(Context context) {
        Singleton result = instance;
        if (result == null) {
            synchronized (Singleton.class) {
                result = instance;
                if (result == null) {
                    instance = result = new Singleton(context);
                }
            }
            //這裡要注意重新賦值
            instance.context = context;
        }
        return result;
    }
}
複製程式碼

鳴謝

  1. Initialization-on-demand holder idiom
  2. Singleton pattern
  3. Head First 設計模式
  4. java-design-patterns

相關文章