設計模式之單例模式,如何破壞單例以及怎么防止


  • 基礎概念

單例模式就是只需要創建一次,在整個應用生命周期都可以一直使用。

我們常分為餓漢式和懶漢式兩種。

餓漢式

餓漢式是在初始化的時候就將單例對象創建出來。通常,通過屬性new創建自身。該方式不存在線程安全的問題(JVM保證線程安全),但會造成內存資源的浪費。

我們可以創建一個這樣的類:

1、定義私有化的成員變量:需初始化,用static修飾。

2、私有化構造器,防止其被其他類new。

3、對外提供公共方法,返回獲取創建好的單例對象,用static修飾。

public class Singleton {

    // 私有化的成員變量:需初始化
    private static Singleton singleton = new Singleton();
    
    // 私有化構造器,防止其被其他類new
    private Singleton() {
        
    }
    
    // 對外提供公共方法,返回獲取創建好的單例對象
    public static Singleton getInstance() {
        
        return singleton;
    }
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
}

 

  • 懶漢式

懶漢式是在第一次使用的時候,才將單例對象創建出來。該方式存在線程安全的問題,但不會造成內存資源的浪費。

我們可以創建一個這樣的類:

1、定義私有化的成員變量:無需初始化,用static修飾。

2、私有化構造器,防止其被其他類new。

3、對外提供公共方法,返回獲取創建好的單例對象。只有當不存在時候才new,存在則直接返回,用static修飾。

public class Singleton {

    // 私有化的成員變量:不做初始化
    private static Singleton singleton = null;
    
    // 私有化構造器,防止其被其他類new
    private Singleton() {
        
    }
    
    // 對外提供公共方法,返回獲取創建好的單例對象
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
}

懶漢式,怎么解決線程安全問題呢?

  • 雙重檢查鎖

兩個線程同時訪問的時候,我們可以加鎖處理。

1、私有化的成員變量:不做初始化,volatile 保證原子性。

2、私有化構造器,防止其被其他類new。

3、對外提供公共方法,返回獲取創建好的單例對象。加兩層非空校驗,將第二層校驗為null的代碼塊用synchronized同步代碼塊。

public class Singleton {

    // 私有化的成員變量:不做初始化
    private volatile static Singleton singleton = null;
    
    // 私有化構造器,防止其被其他類new
    private Singleton() {
        
    }
    
    // 對外提供公共方法,返回獲取創建好的單例對象
    public static Singleton getInstance() {
        // 第一層非空校驗
        if (singleton == null) {
            // 加同步鎖,保證只有一個線程進入
            synchronized (Singleton.class) {
                // 第二層非空校驗,防止在第一次非空校驗時,兩個線程拿到的都是null對象而創建兩次。
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
}

可以了解下對象在JVM中的創建步驟。以及線程相關知識點,同步,怎么保證原子性等。

  • 靜態內部類

1、私有化構造器,防止其被其他類new。

2、使用內部類(JVM保證),創建單例對象。

3、對外提供公共方法,通過調用內部類的屬性,返回獲取的創建好的單例對象。

public class StaticSingleton {
    
    // 私有化構造器,防止其被其他類new
    private StaticSingleton() {
        
    }
    
    // 使用內部類(JVM保證),創建單例對象
    private static class SingletonFactory {
        private static StaticSingleton singleton = new StaticSingleton();
    }
    
    // 對外提供公共方法,通過調用內部類的屬性,返回獲取的創建好的單例對象
    public static StaticSingleton getInstance() {
        
        return SingletonFactory.singleton;
    }
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
}

 

  • 枚舉
public enum SingletonEnum {

    INSTANCE;
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
}

 

問題:使用上面的雙重檢查鎖方式,我們如何破壞單例?

先上代碼

/**
 * 單例攻擊
 */
public class SingletonAttack {

    public static void main(String[] args) throws Exception {
        
//        Singleton2 singleton1 = Singleton2.getInstance();
//        Singleton2 singleton2 = Singleton2.getInstance();
//        System.out.println(singleton1 == singleton2); // true
//        
        reflectAttack();
        
//        reflectAttackWithThread();
        
//        serializationAttack();
    }
    
    /** 反射攻擊測試 */
    public static void reflectAttack() throws Exception {
        Singleton2 singleton1 = Singleton2.getInstance();
        Constructor<Singleton2> constructor = Singleton2.class.getDeclaredConstructor();
        // true-設置成員變量的暴力破解
        constructor.setAccessible(true);
        
//        Singleton2 singleton1 = constructor.newInstance(null);
        
        Singleton2 singleton2 = constructor.newInstance(null);
        
        System.out.println(singleton1 == singleton2);
    }
    
    /** 
     *  序列化深拷貝攻擊測試
     *  如果重寫了readResolve()方法,則執行會報錯ClassCastException
     */
    public static void serializationAttack() throws Exception {
        
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("objectFile"));
        Singleton2 singleton1 = Singleton2.getInstance();
        os.writeObject(singleton1);
        os.close();
        
        ObjectInputStream is = new ObjectInputStream(new FileInputStream(new File("objectFile")));
        Singleton2 singleton2 = (Singleton2) is.readObject();
        is.close();
        
        System.out.println(singleton1 == singleton2);
    }
    
    /** 多線程下反射攻擊測試 */
    public static void reflectAttackWithThread() throws Exception {
        for (int i=0; i<100; i++) {
            Thread thread = new Thread(new Runnable() {
                
                public void run() {
                    try {
                        Constructor<Singleton2> constructor = Singleton2.class.getDeclaredConstructor();
                        // true-設置成員變量的暴力破解
                        constructor.setAccessible(true);
                        Singleton2 singleton = constructor.newInstance(null);
                        System.out.println(singleton);
                    } catch (Exception e) {
                        // 此處方便測試,不打印異常,防止因為異常而終止循環,因為要保證所有線程都能執行
                        // 如果輸出多個不同實例,則表示破壞了單例
                        // 如果只輸出一個實例,表示其他線程都無法獲取實例對象,線程中途出現了異常
                    }
                }
            });
            thread.start();
        }
        
    }
    
}

1.反射,通過反射獲取單例對象的構造器,暴力破解后即可創建多個不同實例。怎么防止:私有構造方法加雙重檢查鎖。

private volatile static boolean isFirstCreate = true;

private Singleton() {
    // 這里雙重校驗,也是防止兩個線程拿到的都是true,而創建了兩個實例
    if (isFirstCreate) {
        synchronized (Singleton.class) {
            if (isFirstCreate) {
                // 為第一次創建,將isFirstCreate設置為true
                isFirstCreate = false;
            } else {
                // isFirstCreate為true,表示之前創建過了,需要拋出異常
                throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!");
            }
        }
    } else {
        // isFirstCreate為true,表示之前創建過了,需要拋出異常
        throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!");
    }
}

 

2.序列化,通過深克隆復制對象,可生成多個實例。怎么防止:重寫在單例對象中readObject()方法。

如果單例對象實現了Serializable接口,我們可以通過在單例對象中重寫readResolve(),禁止程序通過深拷貝創建多個實例,達到破壞單例對象的目的。

private Object readResolve() throws ObjectStreamException {    
    return Singleton.class;
}

優化后的雙重檢查鎖方式的單例類

public class Singleton2 implements Serializable {

    // 私有化的成員變量:不做初始化
    private volatile static Singleton2 singleton = null;
    
    private volatile static boolean isFirstCreate = true;
    
    // 私有化構造器,防止其被其他類new
    private Singleton2() {
        // 這里雙重校驗,也是防止兩個線程拿到的都是true,而創建了兩個實例
        if (isFirstCreate) {
            synchronized (Singleton2.class) {
                if (isFirstCreate) {
                    // 為第一次創建,將isFirstCreate設置為true
                    isFirstCreate = false;
                } else {
                    // isFirstCreate為true,表示之前創建過了,需要拋出異常
                    throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!");
                }
            }
        } else {
            // isFirstCreate為true,表示之前創建過了,需要拋出異常
            throw new RuntimeException("此單例對象已存在,禁止非法調用構造器!");
        }
    }
    
    // 對外提供公共方法,返回獲取創建好的單例對象
    public static Singleton2 getInstance() {
        // 第一層非空校驗
        if (singleton == null) {
            // 加同步鎖,保證只有一個線程進入
            synchronized (Singleton2.class) {
                // 第二層非空校驗,防止在第一次非空校驗時,兩個線程拿到的都是null對象而創建兩次。
                if (singleton == null) {
                    singleton = new Singleton2();
                }
            }
        }
        return singleton;
    }
    
    public void otherMethod() {
        System.out.print("其他的行為方法");
    }
    
    /** 單例對象實現了Serializable接口,通過重寫readResolve()禁止程序通過深拷貝創建多個實例,達到破壞單例對象的目的 */
    private Object readResolve() throws ObjectStreamException {
        
        return Singleton2.class;
    }
}

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM