如何設(shè)計(jì)一個(gè)安全的單例模式

單例模式

單例模式(Singleton Pattern)是 Java 中最簡(jiǎn)單的設(shè)計(jì)模式之一。這種類型的設(shè)計(jì)模式屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。

這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問其唯一的對(duì)象的方式,可以直接訪問,不需要實(shí)例化該類的對(duì)象。

注意:

  • 單例類只能有一個(gè)實(shí)例。
  • 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
  • 單例類必須給所有其他對(duì)象提供這一實(shí)例。

下面是兩種最基礎(chǔ)的單例模式實(shí)現(xiàn)

懶漢式

懶漢式

public class Singleton implements Serializable {
    private volatile static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
}

這種方式是懶漢式最基本的實(shí)現(xiàn)方式,這種實(shí)現(xiàn)最大的問題就是不支持多線程。因?yàn)闆]有加鎖 synchronized,所以嚴(yán)格意義上它并不算單例模式。
這種方式 lazy loading 很明顯,不要求線程安全,在多線程不能正常工作。

餓漢式

餓漢式

public class Singleton implements Serializable  {
    private static final Singleton SINGLETON = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return SINGLETON;
    }
}

餓漢式基于 classloader 機(jī)制避免了多線程的同步問題,不過,instance 在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance() 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化 instance 顯然沒有達(dá)到 lazy loading 的效果。

綜上所述,我們可以看出使用懶漢式的方式創(chuàng)建的單例模式在多線程下并不是安全的,所以提出了使用雙重校驗(yàn)鎖的方式來實(shí)現(xiàn)懶漢式的單例

雙重校驗(yàn)鎖實(shí)現(xiàn)懶漢式

雙重校驗(yàn)鎖實(shí)現(xiàn)懶漢式

public class Singleton implements Serializable {
    private volatile static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            // 加了類鎖,保證了線程安全
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

這種方式采用雙鎖機(jī)制,安全且在多線程情況下能保持高性能。

但是我們想想,這樣做完真的就可以保證程序中有且僅有一個(gè)單例對(duì)象的存在了嗎?答案是否定的,我們可以通過反射的方式來破壞單例

使用反射破壞單例

使用反射破壞單例

// 使用反射來破壞單例模式
public static void main(String[] args) throws Exception {
    // 獲取普通單例對(duì)象
    Singleton instance = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    // 破解 private
    constructor.setAccessible(true);
    // 執(zhí)行構(gòu)造方法創(chuàng)建對(duì)象
    Singleton singleton = constructor.newInstance();
    
    System.out.println(singleton); // Singleton@4554617c
    System.out.println(instance); // Singleton@74a14482
    System.out.println(singleton == instance); // false
}

我們使用反射獲取了 Singleton 的無參構(gòu)造器,通過調(diào)用 setAccessible(true) 忽視 private 權(quán)限控制符,成功的創(chuàng)建了 Singleton 的對(duì)象。反射創(chuàng)建對(duì)象的根本方法就是獲取了 Singleton 的私有構(gòu)造器,那么針對(duì)的方法也就很簡(jiǎn)單了,只需要我們?cè)谒接袠?gòu)造器中再次判斷單例對(duì)象是否已經(jīng)創(chuàng)建,若存在已創(chuàng)建的單例對(duì)象,則拋出運(yùn)行時(shí)異常即可!

解決使用反射破壞單例

public class Singleton implements Serializable {
    private volatile static Singleton singleton;
    private Singleton() {
        // 防止反射調(diào)用無參構(gòu)造器破壞單例模式
        if (singleton != null) {
            throw new RuntimeException("不能重復(fù)創(chuàng)建該對(duì)象");
        }
    }
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}

使用反射再次創(chuàng)建對(duì)象后運(yùn)行截圖如下:


我們現(xiàn)在已經(jīng)防止了多線程下和使用反射來破壞單例了。現(xiàn)在我們的單例模式是不是真的就安全了呢?其實(shí)除了反射以外,使用 序列化與反序列化也同樣可以破壞單例!

使用序列化反序列化破壞單例

使用序列化反序列化破壞單例

public static void main(String[] args) throws Exception {
    Singleton instance = Singleton.getInstance();
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("tempfile"));
    os.writeObject(instance);
    ObjectInputStream is = new ObjectInputStream(new FileInputStream("tempfile"));
    Singleton singleton = (Singleton) is.readObject();
    System.out.println(singleton); // Singleton@4dd8dc3
    System.out.println(instance); // Singleton@7f31245a
    System.out.println(singleton == instance); // false
}

可以看出單例模式又再次被破壞了。為了解決序列化和反序列化破壞單例模式的問題,我們可以定義一種稱為 readResolve() 的特殊序列化方法。如果我們定義了 readResolve() 方法,那么在對(duì)象被序列化后就會(huì)被調(diào)用。

修改 Singleton 類并增加 addResolve() 方法

public class Singleton implements Serializable {
    private volatile static Singleton singleton;

    private Singleton() {
        // 防止反射調(diào)用無參構(gòu)造器破壞單例模式
        if (singleton != null) {
            throw new RuntimeException("不能重復(fù)創(chuàng)建該對(duì)象");
        }
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    /**
     * 防止被序列化破壞單例模式
     * @return
     */
    private Object readResolve() {
        return singleton;
    }

}

再次運(yùn)行序列化和反序列化程序,可以觀察到如下的結(jié)果

不過我們還有一種最簡(jiǎn)單的方式去創(chuàng)建一個(gè)單例模式,那就是使用 枚舉 來創(chuàng)建單例模式

使用枚舉創(chuàng)建單例模式

enum SingletonEnum {
    /**
     * 實(shí)例
     */
    INSTANCE;
    
    private Singleton singleton = null;
    
    SingletonEnum() {
        singleton = new Singleton();
    }
    
    public Singleton getInstance() {
        return singleton;
    }
}

運(yùn)行類 及 結(jié)果

public class Singleton  {
    public static void main(String[] args) {
        Singleton instance = SingletonEnum.INSTANCE.getInstance();
        Singleton singleton = SingletonEnum.INSTANCE.getInstance();
        System.out.println(instance == singleton); //true
    }
}

使用靜態(tài)內(nèi)部類的方式創(chuàng)建單例

通過靜態(tài)內(nèi)部類的方法創(chuàng)建單例同樣實(shí)現(xiàn)了懶加載線程安全,因?yàn)?Java 在加載一個(gè)類時(shí),其內(nèi)部類不會(huì)同時(shí)被加載。一個(gè)類被加載,當(dāng)且僅當(dāng)其某個(gè)靜態(tài)成員(靜態(tài)域、構(gòu)造器、靜態(tài)方法等)被調(diào)用時(shí)發(fā)生。

public class Singleton implements Serializable {
    private Singleton() { }

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

}
最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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