設計模式——單例模式

funtrin發表於2021-10-19

設計模式——單例模式

單例模式,顧名思義就是一個類只能有一個例項。單例模式根據例項的建立的時間大致可以分為兩類——餓漢式單例和懶漢式單例。

餓漢式單例

餓漢式單例,是指在類初始化的時候就建立例項,這樣做有一個好處,就是保證在獲取例項的時候可以保證執行緒安全而且還簡單,即多個執行緒獲取到的都是同一個例項。但這樣做也有一個缺點,就是即使不用例項,例項也會建立,這樣就會造成記憶體浪費。餓漢式單例的簡單實現:

// 餓漢式單例
class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return instance;
    }
}

懶漢式單例

懶漢式單例,見名知意,只有在需要的時候才會建立例項,直接看程式碼:

// 懶漢式單例
class LazySingleton {
    private static LazySingleton singleton;

    private LazySingleton() {

    }

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

很顯然懶漢式單例解決了,餓漢式單例可能出現的佔用記憶體的情況,但是餓漢式單例同樣帶來了獲取單例時執行緒不安全的問題,即可能出現多個執行緒取到的例項不是同一個,最直接的解決方案就是加鎖。

class SynchronizedLazySingleton {
    private static SynchronizedLazySingleton singleton;

    private SynchronizedLazySingleton() {

    }

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

但是加鎖後會使程式碼效能變差。所以我們需要對上面的程式碼做個優化,採用volatile和synchronized配合的方式:

class DoubleLockSingleton {
    // 通過volatile修飾來確保singleton的狀態改變在所有執行緒間可見
    volatile private static DoubleLockSingleton singleton;

    private DoubleLockSingleton() {

    }

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

但是這樣對效能的提升有限,我們可以換一個思路,通過內部類的初始化來優化程式碼

class InnerClassSingleton {
    private InnerClassSingleton() {

    }

    // 靜態內部類只有在使用的時候才會初始化
    public static InnerClassSingleton getInstance() {
        return SingletonHolder.singleton;
    }

    private static class SingletonHolder {
        private static final InnerClassSingleton singleton = new InnerClassSingleton();
    }
}

這樣就可以達到效能與執行緒安全的平衡。

註冊式單例模式

序列化會使單例失效:有時我們需要將單例序列化後寫入磁碟,但是一旦從磁碟中把單例反序列化出來,由於單例的記憶體地址變了,這樣實際上就是建立了一個新的例項,於是只有一個例項的原則就被破壞了。通過列舉類的特性來實現註冊式單例:

enum EnumSingleton {
    SINGLETON;
    
    public EnumSingleton getInstance() {
        return SINGLETON;
    }
}

通過列舉類來實現單例模式,可以保證序列化後得到的是同一個物件,而且列舉類的例項不能通過反射來建立。同樣我們也可以通過hash表來實現註冊式單例:

class HashMapSingleton{
    private static final ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();

    private HashMapSingleton(){

    }

    public static Object getInstance(String className) {
        if (!map.containsKey(className)) {
            Object obj = null;
            try {
                obj = Class.forName(className).newInstance();
                map.put(className, obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return obj;
        }
        return map.get(className);

    }
}

但是通過hash表來實現單例在獲取單例時還是會出現執行緒安全問題。

相關文章