關於單例模式的N種實現方式


  在開發中經常用到單例模式,單例模式也算是設計模式中最容易理解,也是最容易手寫代碼的模式,所以也常作為面試題來考。所以想總結一下單例模式的理論知識,方便同學們面試使用。

  單例模式實現的方式只有兩種類型,一種是餓漢式(類加載時就初始化)、一種是懶漢式(類加載時不初始化)。餓漢式沒什么可講究的因為它既簡單也線程安全,如果條件允許一般我們都會直接用餓漢式;唯獨比較麻煩的是懶漢式,考慮到線程安全,使用懶漢式的單例模式,就有多種實現方式了。

  一、餓漢式

  如上所述,餓漢式很簡單,實現方式如下:

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

  這種單例模式,很簡單而且還安全,可以說近乎完美。它唯一的缺陷就是,這種實現方式就無法適用於單例還需要一定的配置或者傳參的場景。

  二、懶漢式

  懶漢式的單例模式,由於要考慮到線程安全的情況,有多種實現方式。

  0、如下的實現方式是一種不安全的實現方式

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

  之所以把定義為編號0,就是除非你非常確定沒有多線程的場景,因為當有多個線程並行調用 getInstance() 的時候,就會創建多個實例。所以絕大部分場景下,這種實現方式都是不應該使用。

 

  1、低性能的懶漢式

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

  這種實現方式,解決了多線程實例問題,但是由於直接在方法上使用synchronized關鍵字,即類鎖同步,使得只能使用每次調用都要進行同步操作,性能比較低。

 

  2、雙重檢驗鎖的懶漢式

public class Singleton { private volatile static Singleton instance; private Singleton() { } public static Singleton getSingleton() { if (instance == null) { // null 檢測
      synchronized (Singleton.class) { // 同步檢測
        if (instance == null) { instance = new Singleton(); } } } return instance; } }

  我們看到最多的情況就是沒有加volatile的情形,其實你不加這個關鍵字,面試你的人應該也不會糾結於這個,因為99.99%的情況都是好的(要知道那些提供后台服務的,一般只會保證99%可靠性……)。之所以加是因為JVM存在指令重排的優化,導致

new Singleton() 不是一個原子操作行為,new Singleton() 這句話執行的過程大概分為以下abc步驟:

  a, 給 instance 分配內存;

  b, 調用 Singleton 的構造函數來初始化成員變量;

  c, 將instance對象指向分配的內存空間(執行完這步 instance 就為非 null 了);

  JVM 的即時編譯器中存在指令重排序的優化,執行順序可能是 abc 也可能是 acb。如果是acb,在執行到c,被線程X搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程X會直接返回 instance,使用一個沒有初始化的實例產生錯誤是必然的。那為什么要加volatile呢?一般我們只用到volatile的可見性功能,即對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入,而且經常用於解決同步問題。其實volatile還有一個重要的功能——禁止指令重排優化,也就是volatile標記的變量不會被編譯器優化。如上所示,加上volatile以后,讀操作一定會發生在abc或者acb之后,並且這個順序是固定。

 

  3、靜態內部類實現懶漢式

public class Singleton { private static class InternalSingleton { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return InternalSingleton.INSTANCE; } }

  由於InternalSingleton 是私有的,除了 getInstance() 之外沒有辦法訪問它,因此它是懶漢式的;同時讀取實例的時候不會進行同步,沒有性能缺陷。

  4、通過枚舉實現懶漢式
  通過枚舉寫單例超級簡單,什么都不用想,這也是它最大的優點。下面這段代碼就是聲明枚舉實例的通常做法。

public enum Singleton { INSTANCE; public void show(int age) { System.out.println("this is show function -> " + age); } }

  我們可以通過Singleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,所以不需要擔心double checked locking,而且還能防止反序列化導致重新創建新的對象。可能由於平時我們使用枚舉都是為了表示類別,大家都很少使用這種方式去寫單例模式。

 


免責聲明!

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



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