单例模式的挑战:反射和序列化

参考1: 你写的单例模式,能防止反序列化和反射吗?

参考2:枚举实现单例

常见单例模式

// 饿汉,在类加载的时候就被实例化
/**
 * 恶汉式单例,线程安全
 * @author sicimike
 * @create 2020-02-23 20:15
 */
public class Singleton1 {

    private static final Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {}

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

/**
 * 饿汉式单例,静态代码块,线程安全
 * @author sicimike
 * @create 2020-02-23 20:19
 */
public class Singleton2 {

    private static Singleton2 INSTANCE = null;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {}

    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

// 懒汉式单例
/**
 * 懒汉式单例,线程安全
 * 双重校验锁
 * @author sicimike
 * @create 2020-02-23 20:34
 */
public class Singleton6 {

    private static volatile Singleton6 INSTANCE = null;

    private Singleton6() {}

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

不加volatile关键字 线程不安全,根本原因就是INSTANCE = new Singleton5()不是原子操作。而是分为三步完成
1、分配内存给这个对象
2、初始化这个对象
3、把INSTANCE变量指向初始化的对象
正常情况下按照1 -> 2 -> 3的顺序执行,但是2和3可能会发生重排序,执行顺序变成1 -> 3 -> 2。如果是1 -> 3 -> 2的顺序执行。线程A执行完3,此时对象尚未初始化,但是INSTANCE变量已经不为null,线程B执行到synchronized关键字外部的if判断时,就直接返回了。此时线程B拿到的是一个尚未初始化完成的对象,可能会造成安全隐患。所以这种实现方式是线程不安全的。

volatile关键字的在这里的作用有两个:
解决了重排序的问题
保证了INSTANCE的修改,能够及时的被其他线程所知

静态内部类方式
既满足懒加载,又满足线程安全,代码量还少,相对来说是一种比较优雅的实现方式

/**
 * 懒汉式单例,线程安全
 * 静态内部类
 * @author sicimike
 * @create 2020-02-23 20:36
 */
public class Singleton7 {

    private Singleton7() {}

    public static Singleton7 getInstance() {
        return InnerClass.INSTANCE;
    }

    private static class InnerClass {
        private static Singleton7 INSTANCE = new Singleton7();
    }

}

枚举方式

public enum  DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum(){
        connection = new DBConnection();
    }
    public DBConnection getConnection(){
        return connection;
    }
}

public class DBConnection {
}

// 测试 返回 true 
public class Test {
    public static void main(String[] args) {
        DBConnection conn1 = DataSourceEnum.DATASOURCE.getConnection();
        DBConnection conn2 = DataSourceEnum.DATASOURCE.getConnection();
        System.out.println(conn1 == conn2);
    }
}

反射

反射会破坏单例

public void reflectSingleton1(){
    try {

        Object compare1 = Singleton1.getInstance();
        Class<?> tClass = Singleton1.class;
        Constructor constructor = tClass.getDeclaredConstructor(null);
        constructor.setAccessible(true);
        Object instance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(compare1);
        System.out.println(instance == compare1);
    } catch (Exception e){
        e.printStackTrace();
    }
}

// 输出
singleton.Singleton1@27f674d
singleton.Singleton1@1d251891
false

// 添加如下报错内容处理,防止通过反射初始化单例对象,但是不够优雅
private Singleton1() {
    if(INSTANCE != null){
        throw new RuntimeException("do not xia gao");
    }
}    

序列化

序列化也会破坏单例,不再举例。
可在Singleton1内添加如下方法

private Object readResolve(){
    return this.INSTANCE;
}

经过源码查看,若目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类中的readResolve方法,返回一个对象,然后把这个新的对象复制给最终返回的对象。
因此,新建readResolve方法,返回单例类,即保证还是原来创建的类,没有创建新类,是一个对象。

最优解:就是用枚举

// 可以看一下DataSourceEnum类的反编译代码
public final class DataSourceEnum extends Enum
{
    public static DataSourceEnum[] values(){
        return (DataSourceEnum[])$VALUES.clone();
    }
	//toString的逆方法,返回指定名字,给定类的枚举常量
    public static DataSourceEnum valueOf(String name){
        return (DataSourceEnum)Enum.valueOf(creational/singleton/dbconn/DataSourceEnum, name);
    }
	//私有构造函数,参数有 此枚举常量的名称,枚举常量的序号
    private DataSourceEnum(String s, int i){
        super(s, i);
        //单例对象的属性
        connection = null;
        connection = new DBConnection();
    }

    public DBConnection getConnection(){
        return connection;
    }
	//单例对象
    public static final DataSourceEnum DATASOURCE;
    //单例对象的属性
    private DBConnection connection;
    private static final DataSourceEnum $VALUES[];
    static 
    {
    	//与饿汉式相似,类初始化时创建单例对象
        DATASOURCE = new DataSourceEnum("DATASOURCE", 0);
        $VALUES = (new DataSourceEnum[] {
            DATASOURCE
        });
    }
}

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将DATASOURCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

posted @ 2021-06-09 15:24  CalronLoveRonnie  阅读(59)  评论(0编辑  收藏  举报
AmazingCounters.com