網頁

2018/2/24

Java 設計模式 單例模式 Singleton Pattern

Java設計模式的單例模式Singleton Pattern,屬於建構式模式(Creational Patterns)的一種,是常見也是面試時常考的基本設計模式之一。

單例(Singleton)的意思就是只有一個實例。概念同static,是一種全域的表現。 在Java OOP中一個類別(Class)可以產生多個實例(Instance),這些實例彼此是不同的。而單例就是希望應用程式的生命週期中只會有一個實例。

public class Main {

    public static void main(String[] args) {

        Main main1 = new Main(); // 產生一個實例
        Main main2 = new Main(); // 產生另外一個實例

        System.out.println(main1 == main2); //false
    }

}

如果整個應用程式只需要一個東西,那就不用重複產生一模一樣的東西。單例常見的應用場景為:

  • 連線物件
  • 配置物件
  • 日誌物件

單例模式簡單實作如下

public class Singleton {
    
    private static Singleton instance = null;

    private Singleton() {
    }

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

}

在程式中要使用單例類別的物件時,並不直接new新的物件,而是透過公開的靜態方法取得,也就是上面的getInstance()。產生的實例會被放入類別的靜態成員變數中,而靜態成員變數的記憶體位置是固定的,所以之後的程式再透過getInstance()取得實例時,會先判斷之前靜態成員變數中是否存在實例,若不存在則新增一個實例;若存在代則返回的先前產生的實例,而非另外產生一個新的。因為每次都是使用同一個的實例,所以稱作"單例"模式。



以上是非執行緒安全(non-thread-safe)的單例。在多執行緒的程式中,則設計要改為執行緒安全(thread-safe)的單例模式以避免執行緒干涉(thread interference)的問題。

通常碰到執行緒同步的問題會直覺想到用synchronized來確保邏輯的同步,例如直接將取得實例的getInstance()方法改為同步。

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance = null;

    private ThreadSafeSingleton() {
    }

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

}

但此做法的問題是效率差。每次取得單例時都要進入同步方法,而同步取得鎖的成本較高。此外一旦單例物件生成後就沒有必要同步。

因此把同步的範圍縮小到產生實例的部分。當實例已存在直些返回不用進同步,只有生成時才需要同步,此時要利用double check locking的技巧。

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance = null;

    private ThreadSafeSingleton() {
    }

    public static ThreadSafeSingleton getInstance() {
        if(instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }

}

利用執行緒的Class鎖(class level lock),也就是synchronized (ThreadSafeSingleton.class)區塊來限制多執行緒中只允許一個實例存取。

以上的好處是只有第一次產生單例時才進入同步,比起直接在getInstance()方法做同步的效能較好。

或是Class loader載入時直接產生實例。但這樣就沒有懶初始化(lazy-initialization)的效果。

public class ThreadSafeSingleton {

    private static final ThreadSafeSingleton INSTANCE = new ThreadSafeSingleton();

    private ThreadSafeSingleton() {
    }

    public static ThreadSafeSingleton getInstance() {
        return INSTANCE;
    }

}

在Java的Runtime有相同的實現。

java.lang.Runtime

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();
    ...

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {
    }
    ...
}

或是用一個內部靜態類來達到懶初始化的效果,又稱為Bill Pugh Singleton。

public class ThreadSafeSingleton {
    
    private ThreadSafeSingleton() {
    }

    public static ThreadSafeSingleton getInstance() {
        return ThreadSafeSingletonHolder.INSTANCE;
    }

    private static class ThreadSafeSingletonHolder {
        static final ThreadSafeSingleton INSTANCE = new ThreadSafeSingleton(); // 只有當被參考時才會初始化
    }

}

而以上單例的狀態都可能被反射及序列化反序列化破壞。在Effective Java書中建議使用列舉(enum)來保證單例。Enum可確保執行緒安全,單例不會被反射及序列化反序列化破壞。

public enum EnumSingleton {

    INSTANCE;
    
    private int value;

    public void doSomething() {
        // ...
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}


Spring Bean scope預設為singleton,也就是註冊的Bean在Spring IoC容器中只會有一個bean。 但同個類別仍可註冊多個Bean,這和Singleton還是有點差異。


參考:

沒有留言:

張貼留言