單例模式詳解

單例模式是最常用的設(shè)計(jì)模式之一,不管是工作中,還是面試中,單例模式一直都是寵兒。單例模式看似簡(jiǎn)單,但是如果深入理解單例模式的各種實(shí)現(xiàn),會(huì)涉及到很其他方面的知識(shí)點(diǎn),可研究性非常高,而且面試也會(huì)非常喜歡深入去探討單例模式。接下來(lái)講解一下單例模式的各種實(shí)現(xiàn)、優(yōu)缺點(diǎn)、防御等。

定義:保證一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)全局訪問(wèn)點(diǎn)
類型:創(chuàng)建型
適用場(chǎng)景:想要確保任何情況下都絕對(duì)只有一個(gè)實(shí)例
優(yōu)點(diǎn):在內(nèi)存里只有一個(gè)實(shí)例,減少了內(nèi)存開(kāi)銷;可以避免對(duì)資源的多重占用;設(shè)置全局訪問(wèn)點(diǎn),嚴(yán)格控制訪問(wèn)
缺點(diǎn):沒(méi)有接口,擴(kuò)展困難

重點(diǎn):

  1. 私有構(gòu)造器
  2. 線程安全
  3. 延遲加載
  4. 序列化和反序列安全
  5. 反射防御

實(shí)現(xiàn)方式

要保證構(gòu)造器是私有的,這點(diǎn)很重要,所有實(shí)現(xiàn)方式都要遵循這一點(diǎn)。

懶漢模式

懶漢模式即讓實(shí)例延時(shí)加載,在需要的時(shí)候才去創(chuàng)建實(shí)例,減少資源占用

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){}

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

實(shí)例對(duì)象在第一次調(diào)用getInstance方法獲取時(shí)才會(huì)創(chuàng)建,這就是懶加載的思想。但是這種寫(xiě)法在單線程下是沒(méi)問(wèn)題的,但是在多線程下并發(fā)地第一次訪問(wèn)這個(gè)方法會(huì)出現(xiàn)線程安全問(wèn)題,導(dǎo)致創(chuàng)建多個(gè)實(shí)例,優(yōu)化方式就是給獲取對(duì)象的方法加鎖

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){}

    public static synchronized LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}
雙重檢查DoubleCheck

在懶漢模式中,為了解決多線程安全問(wèn)題,給整個(gè)方法都加了鎖,但是效率可以改進(jìn),DoubleCheck寫(xiě)法就是對(duì)懶漢模式的改進(jìn)

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){}
    
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

DoubleCheck寫(xiě)法沒(méi)有鎖住整個(gè)方法,而是鎖住了創(chuàng)建對(duì)象的那部分代碼,減小鎖范圍,增加了效率,且因?yàn)樽隽藘纱慰张袛?,所以得名雙重檢查。

但是這樣寫(xiě)法有一個(gè)指令重排序的問(wèn)題,就是 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton() 這段代碼在JVM執(zhí)行的時(shí)候?qū)嶋H上被拆分為三步,其中2、3兩個(gè)步驟有可能會(huì)被重排序,即3在前,2在后

  1. 給對(duì)象分配內(nèi)存空間
  2. 初始化對(duì)象
  3. 設(shè)置 lazyDoubleCheckSingleton 指向剛剛分配的內(nèi)存地址

as-if-seria語(yǔ)義:為了提高編譯器和處理器的執(zhí)行速度,如果在單線程下程序的結(jié)果不會(huì)被改變,那么就允許對(duì)指令進(jìn)行重排序。

上面說(shuō)的三個(gè)步驟,在單線程下,2、3兩個(gè)步驟即使調(diào)換順序,也不影響使用,所以是被允許重排序的。

但是如果是多線程下,發(fā)生了重排序就會(huì)出現(xiàn)問(wèn)題。比如線程一執(zhí)行到了 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton() 這段代碼,并且發(fā)生了重排序,先執(zhí)行1、3兩個(gè)步驟,還未進(jìn)行初始化。然后此時(shí)線程二執(zhí)行到了第一個(gè)檢查 if(lazyDoubleCheckSingleton == null) ,因?yàn)?lazyDoubleCheckSingleton 已經(jīng)被線程一指向了內(nèi)存地址,所以不為null,所以線程二就直接返回了lazyDoubleCheckSingleton。并且線程二直接就開(kāi)始使用lazyDoubleCheckSingleton對(duì)象了,然而此時(shí)lazyDoubleCheckSingleton還沒(méi)有被初始化,所以線程二直接適用就會(huì)出現(xiàn)異常。

解決辦法就是給lazyDoubleCheckSingleton對(duì)象增加volatile關(guān)鍵字

public class LazyDoubleCheckSingleton {
    private static volatile LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){}

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

volatile 可以禁止指令重排序,所以不會(huì)出現(xiàn)這個(gè)問(wèn)題,如果用DoubleCheck寫(xiě)法實(shí)現(xiàn)單例模式,實(shí)例不加上volatile關(guān)鍵字,一切涼涼。volatile關(guān)鍵字這里不細(xì)講了。

所以在使用懶加載的單例模式時(shí),上面這種寫(xiě)法是最全的,而由于涉及到的知識(shí)面較多,所以這種寫(xiě)法非常適合在面試中小露一手。

靜態(tài)內(nèi)部類實(shí)現(xiàn)

基于類初始化的延遲加載解決方案

public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton(){}
    
    private static class SingletonHelper{
        private static StaticInnerClassSingleton singleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return SingletonHelper.singleton;
    }
}

SingletonHelper是私有內(nèi)部類,外部無(wú)法訪問(wèn)。這種形式在 StaticInnerClassSingleton 類被加載的時(shí)候,內(nèi)部類還不會(huì)被加載,所以內(nèi)部類的實(shí)例對(duì)象還沒(méi)被加載,在調(diào)用getInstance時(shí)候才會(huì)加載SingletonHelper類,從而完成singleton 實(shí)例的創(chuàng)建,從而達(dá)到了延遲加載的目的。

惡漢模式

類加載的時(shí)候就完成實(shí)例化,實(shí)現(xiàn)比較簡(jiǎn)單,缺點(diǎn)就是如果不使用該實(shí)例,就會(huì)造成資源的浪費(fèi)

public class HungrySingleton {
    private HungrySingleton(){}

    private static final HungrySingleton singleton = new HungrySingleton();
    public static HungrySingleton getInstance(){
        return singleton;
    }
}

還有一種變種,將對(duì)象的實(shí)例化移到靜態(tài)代碼塊中

public class HungrySingleton {
    private HungrySingleton(){}
    private final static HungrySingleton singleton;
    static{
        singleton = new HungrySingleton();
    }
    
    public static HungrySingleton getInstance(){
        return singleton;
    }
}

這樣也是在類加載的時(shí)候就會(huì)完成實(shí)例化

破壞單例模式

單例模式的定義就是全局只有一個(gè)實(shí)例,那么想要破壞單例模式,就是創(chuàng)建一個(gè)新的實(shí)例即可。那么創(chuàng)建實(shí)例有哪些方法呢?

序列化與反序列化破壞單例模式

通過(guò)反序列化創(chuàng)建一個(gè)新的實(shí)例,這里使用惡漢模式做測(cè)試,用哪種實(shí)現(xiàn)方式都行,注意給單例類實(shí)現(xiàn) Serializable

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton singleton = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test_data"));
        oos.writeObject(singleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test_data"));
        HungrySingleton newSingleton = (HungrySingleton) ois.readObject();

        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
}

此時(shí)打印出來(lái)的結(jié)果是

design_pattern.singleton.HungrySingleton@4b67cf4d
design_pattern.singleton.HungrySingleton@16b98e56
false

可以看到兩個(gè)實(shí)例不一樣,破壞了單例模式的全局唯一性。

解決方案如下,在單例類里面增加一個(gè) readResolve() 方法,返回唯一的那個(gè)實(shí)例

public class HungrySingleton implements Serializable {
    private HungrySingleton(){}
    private static final HungrySingleton singleton = new HungrySingleton();
    public static HungrySingleton getInstance(){
        return singleton;
    }
    // 增加該方法
    private Object readResolve(){
        return singleton;
    }
}

這時(shí)候再執(zhí)行測(cè)試代碼,結(jié)果兩個(gè)實(shí)例就是一致的

design_pattern.singleton.HungrySingleton@4b67cf4d
design_pattern.singleton.HungrySingleton@4b67cf4d
true

反序列化的時(shí)候原本是創(chuàng)建一個(gè)新的實(shí)例,但是如果增加了這個(gè)方法,就會(huì)反射調(diào)用該方法,替換掉原本創(chuàng)建的那個(gè)實(shí)例,實(shí)際上是有創(chuàng)建了新的實(shí)例,但是沒(méi)有被返回。源碼就不細(xì)講了。

當(dāng)單例類有涉及到序列化與反序列化時(shí)一定要注意反序列化對(duì)單例模式的破壞。

反射攻擊破壞單例模式

利用反射創(chuàng)建新的實(shí)例,雖然構(gòu)造方法已經(jīng)設(shè)為private,但是反射依舊可以修改訪問(wèn)權(quán)限,這里依然使用惡漢模式做一下測(cè)試

        HungrySingleton instance = HungrySingleton.getInstance();

        Class clz = HungrySingleton.class;
        Constructor constructor = clz.getDeclaredConstructor();
        constructor.setAccessible(true);    // 打開(kāi)構(gòu)造方法的訪問(wèn)權(quán)限
        Object newInstance = constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);

打印出來(lái)的結(jié)果如下:

design_pattern.singleton.HungrySingleton@6d6f6e28
design_pattern.singleton.HungrySingleton@135fbaa4
false

可以看到已經(jīng)出現(xiàn)了兩個(gè)實(shí)例,破壞了單例模式。

那有沒(méi)有辦法防御這種破壞呢?在惡漢模式中,實(shí)例只會(huì)在類加載的時(shí)候被創(chuàng)建,后面在使用單例類的時(shí)候,實(shí)際上實(shí)例已經(jīng)存在了。那么我們可以在構(gòu)造方法中直接拋出異常,阻止用構(gòu)造方法創(chuàng)建實(shí)例。

public class HungrySingleton{
    private HungrySingleton(){
        if(singleton != null){
            throw new RuntimeException("禁止用構(gòu)造器創(chuàng)建實(shí)例");
        }
    }
    private static final HungrySingleton singleton = new HungrySingleton();
    public static HungrySingleton getInstance(){
        return singleton;
    }
}

再次執(zhí)行測(cè)試類,就會(huì)拋出異常。靜態(tài)內(nèi)部類實(shí)現(xiàn)方式也可以用這種方式防御。

但是在懶加載的模式下,例如 LazySingleton 單例類被加載的時(shí)候,singleton 實(shí)例是還沒(méi)有被初始化的,在構(gòu)造方法中拋異常這種方法是無(wú)法做到真正預(yù)防,如果是先調(diào)用 LazySingleton.getInstance() 創(chuàng)建實(shí)例,再反射調(diào)用構(gòu)造方法是可行的,因?yàn)檎{(diào)用完 LazySingleton.getInstance() 方法之后,實(shí)例就被創(chuàng)建了,此時(shí)構(gòu)造方法就會(huì)拋異常。但是如果先用反射調(diào)用構(gòu)造方法創(chuàng)建實(shí)例的話就無(wú)法預(yù)防到了。

所以懶加載實(shí)現(xiàn)的單例模式還是存在安全隱患的。

字節(jié)碼技術(shù)破壞單例模式

如果用字節(jié)碼技術(shù)可以動(dòng)態(tài)修改字節(jié)碼,也就是class文件,想怎么改就怎么改,這種方式應(yīng)該是沒(méi)法防御的。。。

枚舉實(shí)現(xiàn)單例模式

《Effective Java》中推薦使用枚舉的方式來(lái)實(shí)現(xiàn)單例模式

public enum EnumInstance {
    INSTANCE{
        @Override
        public void print() {
            System.out.println("用枚舉實(shí)現(xiàn)單例模式");
        }
    };
    private Object data;
    public abstract void print();

    public Object getData() { return data; }
    public void setData(Object data) { this.data = data; }
}

這種實(shí)現(xiàn)方式符合全局唯一性,且無(wú)懼 序列化與反序列化 和 反射 的攻擊。

參考:
慕課網(wǎng) Geely 老師的《Java設(shè)計(jì)模式精講 Debug方式+內(nèi)存分析》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容