淺談23種設計模式之單例設計模式

語言: CN / TW / HK

單例模式

1、簡介

單例模式是設計模式中最簡單的形式之一。這一模式的目的是使得類的一個物件成為系統中的唯一例項。要實現這一點,可以從客戶端對其進行例項化開始。因此需要用一種只允許生成物件類的唯一例項的機制,“阻止”所有想要生成物件的訪問。使用工廠方法來限制例項化過程。這個方法應該是靜態方法(類方法),因為讓類的例項去生成另一個唯一例項毫無意義。

2、應用場景

舉一個例子,網站的計數器,一般也是採用單例模式實現,如果你存在多個計數器,每一個使用者的訪問都重新整理計數器的值,這樣的話你的實計數的值是難以同步的。但是如果採用單例模式實現就不會存在這樣的問題,而且還可以避免執行緒安全問題。同樣多執行緒的執行緒池的設計一般也是採用單例模式,這是由於執行緒池需要方便對池中的執行緒進行控制

同樣,對於一些應用程式的日誌應用,或者web開發中讀取配置檔案都適合使用單例模式,如HttpApplication 就是單例的典型應用。

3、優缺點

優點:

  • 在記憶體中只有一個物件,節省記憶體空間;
  • 避免頻繁的建立銷燬物件,可以提高效能;
  • 避免對共享資源的多重佔用,簡化訪問;
  • 為整個系統提供一個全域性訪問點。

缺點:

  • 不適用於變化頻繁的物件;
  • 濫用單例將帶來一些負面問題,如為了節省資源將資料庫連線池物件設計為的單例類,可能會導致共享連線池物件的程式過多而出現連線池溢位;
  • 如果例項化的物件長時間不被利用,系統會認為該物件是垃圾而被回收,這可能會導致物件狀態的丟失;

4、實現方式

4.1 餓漢式

/**
 * 餓漢式
 * 類載入到記憶體後,就例項化一個單例,JVM保證執行緒安全
 * 簡單實用,推薦使用!
 * 唯一缺點:不管用到與否,類裝載時就完成例項化
 */
public class Mgr01 {
    
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

在開發中,推薦使用這種方式,簡單易用,如果追求更高的要求,可以參考以下懶漢式的實現方式。

4.2 懶漢式

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來執行緒不安全的問題
 * 可以通過synchronized解決,但也帶來效率下降
 */
public class Mgr06 {
    
    private static volatile Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //雙重檢查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

雙重檢查,同時對部分程式碼進行鎖定,效率上有點影響,在看另外一種實現方式

/**
 * 靜態內部類方式
 * JVM保證單例
 * 載入外部類時不會載入內部類,這樣可以實現懶載入
 */
public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {
        private final static Mgr07 INSTANCE = new Mgr07();
    }

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr07.getInstance().hashCode());
            }).start();
        }
    }
}

實際開發中,可以根據專案的業務場景進行選擇。