面試中單例模式有幾種寫法?

“你知道茴香豆的‘茴’字有幾種寫法嗎?”

糾結單例模式有幾種寫法有用嗎?有點用,面試中經常選擇其中一種或幾種寫法作為話頭,考查設計模式和coding style的同時,還很容易擴展到其他問題。這里講解幾種猴哥常用的寫法,但切忌生搬硬套,去記“茴香豆的寫法”。編程最大的樂趣在于“know everything, control everything”。

JDK版本:oracle java 1.8.0_102

大體可分為4類,下面分別介紹他們的基本形式、變種及特點。

飽漢模式

飽漢是變種最多的單例模式。我們從飽漢出發,通過其變種逐漸了解實現單例模式時需要關注的問題。

基礎的飽漢

飽漢,即已經吃飽,不著急再吃,餓的時候再吃。所以他就先不初始化單例,等第一次使用的時候再初始化,即“懶加載”。

// 飽漢
// UnThreadSafe
public class Singleton1 {
  private static Singleton1 singleton = null;

  private Singleton1() {
  }

  public static Singleton1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1();
    }
    return singleton;
  }
}

飽漢模式的核心就是懶加載。好處是更啟動速度快、節省資源,一直到實例被第一次訪問,才需要初始化單例;小壞處是寫起來麻煩,大壞處是線程不安全,if語句存在競態條件。

寫起來麻煩不是大問題,可讀性好啊。因此,單線程環境下,基礎飽漢是猴哥最喜歡的寫法。但多線程環境下,基礎飽漢就徹底不可用了。下面的幾種變種都在試圖解決基礎飽漢線程不安全的問題。

飽漢 - 變種 1

最粗暴的犯法是用synchronized關鍵字修飾getInstance()方法,這樣能達到絕對的線程安全。

// 飽漢
// ThreadSafe
public class Singleton1_1 {
  private static Singleton1_1 singleton = null;

  private Singleton1_1() {
  }

  public synchronized static Singleton1_1 getInstance() {
    if (singleton == null) {
      singleton = new Singleton1_1();
    }
    return singleton;
  }
}

變種1的好處是寫起來簡單,且絕對線程安全;壞處是并發性能極差,事實上完全退化到了串行。單例只需要初始化一次,但就算初始化以后,synchronized的鎖也無法避開,從而getInstance()完全變成了串行操作。性能不敏感的場景建議使用。

飽漢 - 變種 2

變種2是“臭名昭著”的DCL 1.0。

針對變種1中單例初始化后鎖仍然無法避開的問題,變種2在變種1的外層又套了一層check,加上synchronized內層的check,即所謂“雙重檢查鎖”(Double Check Lock,簡稱DCL)。

// 飽漢
// UnThreadSafe
public class Singleton1_2 {
  private static Singleton1_2 singleton = null;

  private Singleton1_2() {
  }

  public static Singleton1_2 getInstance() {
    // may get half object
    if (singleton == null) {
      synchronized (Singleton1_2.class) {
        if (singleton == null) {
          singleton = new Singleton1_2();
        }
      }
    }
    return singleton;
  }
}

變種2的核心是DCL,看起來變種2似乎已經達到了理想的效果:懶加載+線程安全??上У氖?,正如注釋中所說,DCL仍然是線程不安全的,由于指令重排序,你可能會得到“半個對象”。詳細在看完變種3后,可參考猴子之前的一篇文章,這里不再贅述。

參考:volatile關鍵字的作用、原理

飽漢 - 變種 3

變種3專門針對變種2,可謂DCL 2.0

針對變種3的“半個對象”問題,變種3在instance上增加了volatile關鍵字,原理見上述參考。

// 飽漢
// ThreadSafe
public class Singleton1_3 {
  private static volatile Singleton1_3 singleton = null;

  private Singleton1_3() {
  }

  public static Singleton1_3 getInstance() {
    if (singleton == null) {
      synchronized (Singleton1_3.class) {
        // must be a complete instance
        if (singleton == null) {
          singleton = new Singleton1_3();
        }
      }
    }
    return singleton;
  }
}

多線程環境下,變種3更適用于性能敏感的場景。但后面我們將了解到,就算是線程安全的,還有一些辦法能破壞單例。

餓漢模式

與飽漢相對,餓漢很餓,只想著盡早吃到。所以他就在最早的時機,即類加載時初始化單例,以后訪問時直接返回即可。

// 餓漢
// ThreadSafe
public class Singleton2 {
  private static final Singleton2 singleton = new Singleton2();

  private Singleton2() {
  }

  public static Singleton2 getInstance() {
    return singleton;
  }
}

餓漢的好處是天生的線程安全(得益于類加載機制),寫起來超級簡單,使用時沒有延遲;壞處是有可能造成資源浪費(如果類加載后就一直不使用單例的話)。

值得注意的時,單線程環境下,餓漢與飽漢在性能上沒什么差別;但多線程環境下,由于飽漢需要加鎖,餓漢的性能反而更優。

Holder模式

我們既希望利用餓漢模式中靜態變量的方便和線程安全;又希望通過懶加載規避資源浪費。Holder模式滿足了這兩點要求:核心仍然是靜態變量,足夠方便和線程安全;通過靜態的Holder類持有真正實例,間接實現了懶加載。

// Holder模式
// ThreadSafe
public class Singleton3 {
  private static class SingletonHolder {
    private static final Singleton3 singleton = new Singleton3();

    private SingletonHolder() {
    }
  }


  private Singleton3() {
  }

  /**
  * 勘誤:多寫了個synchronized。。
  public synchronized static Singleton3 getInstance() {
    return SingletonHolder.singleton;
  }
  */

  public static Singleton3 getInstance() {
    return SingletonHolder.singleton;
  }
}

相對于餓漢模式,Holder模式僅增加了一個靜態內部類的成本,與飽漢的變種3效果相當(略優),都是比較受歡迎的實現方式。同樣建議考慮。

枚舉模式

用枚舉實現單例模式,相當好用,但可讀性是不存在的。

基礎的枚舉

將枚舉的靜態成員變量作為單例的實例:

// 枚舉
// ThreadSafe
public enum Singleton4 {
  SINGLETON;
}

代碼量比餓漢模式更少。但用戶只能直接訪問實例Singleton4.SINGLETON——事實上,這樣的訪問方式作為單例使用也是恰當的,只是犧牲了靜態工廠方法的優點,如無法實現懶加載。

丑陋但好用的語法糖

Java的枚舉是一個“丑陋但好用的語法糖”。

枚舉型單例模式的本質

通過反編譯(jad,源碼|String拼接操作”+”的優化?也用到了)打開語法糖,就看到了枚舉類型的本質,簡化如下:

// 枚舉
// ThreadSafe
public class Singleton4 extends Enum<Singleton4> {
  ...
  public static final Singleton4 SINGLETON = new Singleton4();
  ...
}

本質上和餓漢模式相同,區別僅在于公有的靜態成員變量。

用枚舉實現一些trick

這一部分與單例沒什么關系,可以跳過。如果選擇閱讀也請認清這樣的事實:雖然枚舉相當靈活,但如何恰當的使用枚舉有一定難度。一個足夠簡單的典型例子是TimeUnit類,建議有時間耐心閱讀。

上面已經看到,枚舉型單例的本質仍然是一個普通的類。實際上,我們可以在枚舉型型單例上增加任何普通類可以完成的功能。要點在于枚舉實例的初始化,可以理解為實例化了一個匿名內部類。為了更明顯,我們在Singleton4_1中定義一個普通的私有成員變量,一個普通的公有成員方法,和一個公有的抽象成員方法,如下:

// 枚舉
// ThreadSafe
public enum Singleton4_1 {
  SINGLETON("enum is the easiest singleton pattern, but not the most readable") {
    public void testAbsMethod() {
      print();
      System.out.println("enum is ugly, but so flexible to make lots of trick");
    }
  };

  private String comment = null;

  Singleton4_1(String comment) {
    this.comment = comment;
  }

  public void print() {
    System.out.println("comment=" + comment);
  }

  abstract public void testAbsMethod();


  public static Singleton4_1 getInstance() {
    return SINGLETON;
  }
}

這樣,枚舉類Singleton4_1中的每一個枚舉實例不僅繼承了父類Singleton4_1的成員方法print(),還必須實現父類Singleton4_1的抽象成員方法testAbsMethod()。

總結

上面的分析都忽略了反射和序列化的問題。通過反射或序列化,我們仍然能夠訪問到私有構造器,創建新的實例破壞單例模式。此時,只有枚舉模式能天然防范這一問題。反射和序列化猴子還不太了解,但基本原理并不難,可以在其他模式上手動實現。

下面繼續忽略反射和序列化的問題,做個總結回味一下:

實現方式 關鍵點 資源浪費 線程安全 多線程環境的性能足夠優化
基礎飽漢 懶加載 -
飽漢變種1 懶加載、同步
飽漢變種2 懶加載、DCL -
飽漢變種3 懶加載、DCL、volatile
餓漢 靜態變量初始化
Holder 靜態變量初始化、holder
枚舉 枚舉本質、靜態變量初始化

單例模式是面試中的??键c,寫起來非常簡單。一方面考查正確性,看本文分析;一方面考查coding style,參考:程序猿應該記住的幾條基本規則


本文鏈接:面試中單例模式有幾種寫法?
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協議發布,歡迎轉載,演繹或用于商業目的,但是必須保留本文的署名及鏈接。

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

推薦閱讀更多精彩內容

  • 單例模式(SingletonPattern)一般被認為是最簡單、最易理解的設計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,293評論 4 34
  • 概念 確保某一個類只有一個實例,而且自行實例化,并向整個系統提供一個訪問它的全局訪問點,這個類稱為單例類。 特性 ...
    野狗子嗷嗷嗷閱讀 558評論 0 2
  • 1.單例模式概述 (1)引言 單例模式是應用最廣的模式之一,也是23種設計模式中最基本的一個。本文旨在總結通過Ja...
    曹豐斌閱讀 3,005評論 6 47
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,538評論 1 8
  • 版權聲明:本文為博主原創文章,未經博主允許不得轉載 PS:轉載請注明出處作者: TigerChain地址: htt...
    TigerChain閱讀 1,344評論 0 3