首页 > 技术文章 > 设计模式之单例模式,如何破坏单例以及怎么防止

scorpio-cat 2020-04-15 19:14 原文

  • 基础概念

单例模式就是只需要创建一次,在整个应用生命周期都可以一直使用。

我们常分为饿汉式和懒汉式两种。

饿汉式

饿汉式是在初始化的时候就将单例对象创建出来。通常,通过属性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;
    }
}

 

 

 

推荐阅读