设计模式-单例模式(包括反射和序列化的影响和解决方法)

文章目录

  • 前言
  • 1. 单例模式的介绍
  • 2. 代码
    • 1. 懒汉式,线程不安全
    • 2. 懒汉式,线程安全
    • 3. 饿汉式
    • 4、双检锁/双重校验锁(DCL,即 double-checked locking)
    • 5、登记式/静态内部类
    • 6、枚举
  • 3. 反射对单例的影响
    • 1. 反射破坏单例
    • 2. 如何防止反射破坏单例
    • 3. Enum枚举类为什么可以防止反射破坏单例
  • 4. 谈谈单例的序列化
    • 1. 测试序列化和反序列化
    • 2. 反序列化为什么会破坏单例
    • 3. 解决方法
    • 4. 枚举类的序列化
  • 5. 总结


前言

在之前写过一篇java单例模式的文章,只是简单介绍两种懒汉式和饿汉式的单例,因此在这里重新写一次单例模式,本篇文章基于菜鸟教程的单例模式,会介绍其中的6种单例模式,并且会涉及到多线程情况下的并发问题
菜鸟教程:菜鸟教程-单例模式




1. 单例模式的介绍

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,也是比较常见的几种设计模式之一。菜鸟教程上的单例模式给出了6中实现单例模式的方法。这种模式用于一个类创建自己的单一对象并且给外界提供单一的访问接口,具有以下特点。
       1、单例类只能有一个实例。
       2、单例类必须自己创建自己的唯一实例。
       3、单例类必须给所有其他对象提供这一实例

使用场景: 对于一个类的频繁销毁和创建,可以使用单例模式来节省系统资源。

优点:
1 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2. 避免对资源的多重占用(比如写文件操作)。

缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:java中的线程池,springboot使用Autowired注解,网站的计数器,日志应用,数据库连接池等等


单例的实现步骤
1 .提供一个单例类
2 .提供该类的私有构造器
3 .提供一个静态方法获取该类的唯一实例对象



2. 代码

1. 懒汉式,线程不安全

是否 Lazy 初始化:是
是否多线程安全:否
实现难度:易

这种模式是最简单的单线程模式,由于不考虑多线程的问题,所以没有加Synchronized锁,只在单线程下是线程安全的,严格意义上来讲不能算是单例模式,在多线程下是不起作用的。

//单线程
public class SingletonTest {
    public static void main(String[] args) {
        single instance = single.getInstance();
        single instance1 = single.getInstance();
        //单线程相同
        System.out.println(instance);   //com.jianglianghao.single@14ae5a5
        System.out.println(instance1);  //com.jianglianghao.single@14ae5a5
    }
}

class single{

    private static single instance;

    private single(){}

    public static single getInstance(){
        if(instance == null){
            instance = new single();
        }
        return instance;
    }
}
//多线程
public class SingletonTest {
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                System.out.println("线程:t1 ==> " + single.getInstance());
            }, "t1").start();
        }
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                System.out.println("线程:t2 ==> " +  single.getInstance());
            }, "t1").start();
        }
        //展示其中一部分,多线程情况下,单例失效
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@49b7d06d
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@36c89649
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
    }
}

class single{

    private static single instance;

    private single(){}

    public static single getInstance(){
        if(instance == null){
            instance = new single();
        }
        return instance;
    }
}



2. 懒汉式,线程安全

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:易

该方法使用了synchronized锁对多线程来确保安全,但是由于有synchronized,导致了性能下降。在第一次调用getInstance方法时候开始创建,减少内存损耗。这种方法对比第4种双重锁的方法效率要低不少,最重要的原因就是每个线程都要获取锁,判断if条件,然后释放锁。

//单线程
public class SingletonTest {
    public static void main(String[] args) {
        single instance = single.getInstance();
        single instance1 = single.getInstance();
        System.out.println(instance);
        //com.jianglianghao.single@14ae5a5
        System.out.println(instance1);
        //com.jianglianghao.single@14ae5a5
    }
}

class single{

    private static single instance;

    private single(){}

    public static synchronized single getInstance(){
        if(instance == null){
            instance = new single();
        }
        return instance;
    }
}
//多线程,线程安全
public class SingletonTest {
    public static void main(String[] args) {
        for(int i = 0; i < 1000; i++){
            new Thread(()->{
                System.out.println("线程:t1 ==> " + single.getInstance());
            }, "t1").start();
        }
        for(int i = 0; i < 1000; i++) {
            new Thread(() -> {
                System.out.println("线程:t2 ==> " + single.getInstance());
            }, "t1").start();
        }
        //线程:t1 ==> com.jianglianghao.single@22ff7883
		//线程:t1 ==> com.jianglianghao.single@22ff7883
		//线程:t1 ==> com.jianglianghao.single@22ff7883
		//线程:t1 ==> com.jianglianghao.single@22ff7883
		//...	
    }
}

class single{

    private static single instance;

    private single(){}

    public static synchronized single getInstance(){
        if(instance == null){
            instance = new single();
        }
        return instance;
    }
}



3. 饿汉式

是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易

这种方法好处就是它基于 classloader 机制避免了多线程的同步问题,不用考虑加锁这些,但是如果一个对象在类加载的时候就加载了,而在后续的过程中不使用,就容易产生垃圾对象。按菜鸟教程的说法:虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

//单线程下:
public class SingletonTest {
    public static void main(String[] args) {
        System.out.println(single.getInstance());
        //com.jianglianghao.single@14ae5a5
        System.out.println(single.getInstance());
        //com.jianglianghao.single@14ae5a5
    }
}

class single{

    private static single instance = new single();

    private single(){}

    public static synchronized single getInstance(){
        return instance;
    }
}
//多线程下:
public class SingletonTest {
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                System.out.println("线程:t1 ==> " + single.getInstance());
            }, "t1").start();
        }
        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println("线程:t2 ==> " + single.getInstance());
            }, "t1").start();
        }
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t1 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
        //线程:t2 ==> com.jianglianghao.single@22ff7883
    }
}

class single{

    private static single instance = new single();

    private single(){}

    public static synchronized single getInstance(){
        return instance;
    }
}



4、双检锁/双重校验锁(DCL,即 double-checked locking)

JDK 版本:JDK1.5 起
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂

这种方法使用两个if和synchronized锁进行实例化,效率比第2种高,原因在以前写过的一篇文章讲过:单例模式,这里不再重复。而在这种单例模式中还有一个比较重要的点是使用了volatile 这个关键字。这个关键字的其中的一个关键作用就是防止指令重排序,这个作用可以防止我们获取到一个空的单实例,具体是怎么样的这里不多说,将会在Java多线程这一系列的博客中详细谈这个关键字。

//单线程
public class SingletonTest {
    public static void main(String[] args) {
        single instance = single.getInstance();
        single instance1 = single.getInstance();
        //com.jianglianghao.single@14ae5a5
        System.out.println(instance);
        //com.jianglianghao.single@14ae5a5
        System.out.println(instance1);

    }
}

class single{

    private static volatile single instance;

    private single(){}

    public static single getInstance(){
        if(instance == null){
            synchronized (single.class){
                if(instance == null){
                    instance = new single();
                }
            }
        }
        return instance;
    }
}
//多线程
public class SingletonTest {
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                System.out.println("线程:t1 ==> " + single.getInstance());
            }, "t1").start();
        }
        for(int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println("线程:t2 ==> " + single.getInstance());
            }, "t1").start();
        }
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t1 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //线程:t2 ==> com.jianglianghao.single@7dff9960
        //
        //Process finished with exit code 0
    }
}

class single{

    private static volatile single instance;

    private single(){}

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



5、登记式/静态内部类

是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,但是这种方法的特点就是类里面还包含一个静态内部类,所以在初始化类的时候实例还没有被初始化,只是等到显式调用的时候内部静态类的获取对象方法的时候才会初始化实例。

public class SingletonStaticClass {
    private SingletonStaticClass(){}
    
    //私有的静态内部类
    private static class Instance{
        //返回SingletonStaticClass的静态实例
        private static final SingletonStaticClass INSTANCE = new SingletonStaticClass();
    }
    //调用getInstance的时候才回去初始化Instance这个类内部的INSTANCE方法
    public static final SingletonStaticClass getInstance() {
        return Instance.INSTANCE;
    }
}



6、枚举

JDK 版本:JDK1.5 起
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易

这时目前来说最值得推崇的一个方法,首先更简洁了,《Effective Java》这本书谈到这种方式能自动支持序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。但是对于这种方法,如果需要进行扩展,那么这种方法是不适合的。

public enum SignleEnum {
    INSTANCE("小明", "13"),

    INSTANCE2("小红", "14");

    private String name;
    private String age;

    SignleEnum() {
    }

    @Override
    public String toString() {
        return "SignleEnum{" +
                "name='" + name + '\'' +
                ", age='" + age + '\'' +
                '}';
    }

    SignleEnum(String name, String age) {
        this.name = name;
        this.age = age;
    }

    public static SignleEnum getInstance(){
        return INSTANCE;
    }

    public static SignleEnum getInstance2(){
        return INSTANCE2;
    }

}

测试:

public class TestSingleton {
    public static void main(String[] args) {
        SignleEnum instance = SignleEnum.getInstance();
        SignleEnum instance2 = SignleEnum.getInstance2();
        System.out.println("instance : " + instance.toString());
        System.out.println("instance2 : " + instance2.toString());
        //instance : SignleEnum{name='小明', age='13'}
        //instance2 : SignleEnum{name='小红', age='14'}
    }
}



3. 反射对单例的影响

1. 反射破坏单例

其实对于最后一种的枚举单例,反射是破坏不了的 ,下面以第四种双重锁DCL为例说明

public class LazySafe02 {

    private LazySafe02(){}

    private volatile static LazySafe02 lazySafe02 = null;

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

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        //获取类的构造器
        Constructor<LazySafe02> constructor = LazySafe02.class.getDeclaredConstructor();
        //设置权限
        constructor.setAccessible(true);
        //使用 constructor 创造对象
        LazySafe02 lazySafe02 = constructor.newInstance();
        LazySafe02 lazySafe021 = constructor.newInstance();
        System.out.println(lazySafe02);
        System.out.println(lazySafe021);
    }
}

结果的两个对象:从这里也可以看出来其实就不是一个对象来的,其实单例模式创建不了对象就是因为里面的构造器拿不到,这里反射就直接拿到了构造器了,那么要想防止这种情况,我们可以从构造器那里入手。
设计模式-单例模式(包括反射和序列化的影响和解决方法)_第1张图片



2. 如何防止反射破坏单例

思路就是:可以在构造器里面判断如果创建了对象,那么就抛出异常,但是值得注意的是,我们判断有没有创建了对象,一定不能判断里面的单例是不是null, 就比如下面这种方法,是不可行的,因为如果自己的程序本身没有用到实例,在这种情况下,如果lazySafe02 是空的,那么只要一直通过反射创建就一直不会抛出异常。

//使用 constructor 创造对象, 反射在先,判断空也没有用
LazySafe02 lazySafe02 = constructor.newInstance();
LazySafe02 lazySafe021 = constructor.newInstance();
LazySafe02 instance = LazySafe02.getInstance();

下面是错误的实例:

public class LazySafe02 {

    private LazySafe02() throws Exception {
        if(lazySafe02 != null){
            throw new Exception("不能通过反射创建多个对象....");
        }
    }

    private volatile static LazySafe02 lazySafe02 = null;

    public static LazySafe02 getInstance() throws Exception {
        if(lazySafe02 == null){
            synchronized (LazySafe02.class) {
                if (lazySafe02 == null) {
                    lazySafe02 = new LazySafe02();
                }
            }
        }
        return lazySafe02;
    }
}


其实到这里也不难发现了,通过反射创建实例最终还是调用的这个类的构造器,所以我们可以通过一个计数器来实现功能。当然了构造器里面也没必要用多线程了,因为count是一个静态成员变量来的,加了volatile 之后是线程安全的。

public class LazySafe02 {

    private static volatile int count = 0;

    private LazySafe02() throws Exception {
        count ++;
        if(count > 1){
            throw new Exception("不能使用反射破坏单例");
        }
    }

    private volatile static LazySafe02 lazySafe02 = null;

    public static LazySafe02 getInstance() throws Exception {
        if(lazySafe02 == null){
            synchronized (LazySafe02.class) {
                if (lazySafe02 == null) {
                    lazySafe02 = new LazySafe02();
                }
            }
        }
        return lazySafe02;
    }
}

设计模式-单例模式(包括反射和序列化的影响和解决方法)_第2张图片

当然了count变量要定义成静态的,否则就没效果了,比如下面这样的,还是可以反射创建实例

 private  int count = 0;

 private LazySafe02() throws Exception {
   count ++;
     if(count > 1){
         throw new Exception("不能使用反射破坏单例");
     }
 }



3. Enum枚举类为什么可以防止反射破坏单例

枚举类调用反射创建的时候会直接抛出异常 Exception in thread “main” java.lang.NoSuchMethodException

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        //获取类的构造器
        Constructor<SignleEnum> constructor = SignleEnum.class.getDeclaredConstructor();
        //设置权限
        constructor.setAccessible(true);
        //使用 constructor 创造对象
        SignleEnum lazySafe02 = constructor.newInstance();
        SignleEnum instance = SignleEnum.getInstance();
        
        System.out.println(lazySafe02);
    }
}



我们可以进入源码观察下:其实中间对枚举类做了判断,如果是枚举类,就直接抛出异常

 @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这一行其实就对枚举累做了限制了
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }



4. 谈谈单例的序列化

1. 测试序列化和反序列化

下面测试测试单例模式的序列化和反序列化:不难发现其实输入的和输出的不是同一个对象了,那么又是什么原因呢?

public class TestSerializable {
    public static void main(String[] args) throws Exception {

        LazySafe02 before = LazySafe02.getInstance();

        //我们把对象输出到一个文件上
        FileOutputStream fileOutputStream = new FileOutputStream("SingleTon.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(before);

        //再读取出来
        FileInputStream fileInputStream = new FileInputStream("SingleTon.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object instance2 = objectInputStream.readObject();
        LazySafe02 after = (LazySafe02)instance2;

        System.out.println(before);
        System.out.println(after);
        //com.jianglianghao.singletonDesigned.LazySafe02@7440e464
        //com.jianglianghao.singletonDesigned.LazySafe02@4fca772d
    }
}

在这里插入图片描述



2. 反序列化为什么会破坏单例

其实看上面的代码不难发现,我们写入的时候可以,但是读出来的时候就不行了,所以把把注意力集中在readObject这个方法中,我们通过调试查明原因。


1、在readObject方法中我们定位到Object obj = readObject0(type, false)这一句

  private final Object readObject(Class<?> type)
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        if (! (type == Object.class || type == String.class))
            throw new AssertionError("internal error");

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
        	//定位到这里
            Object obj = readObject0(type, false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

2、在readObject0方法中定位到case TC_ARRAY: 这一行,执行
return checkResolve(readOrdinaryObject(unshared))

private Object readObject0(Class<?> type, boolean unshared) throws IOException {
	....
	.... 省略
	....

 switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    // check the type of the existing object
                    return type.cast(readHandle(unshared));

                case TC_CLASS:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast a class to java.lang.String");
                    }
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast a class to java.lang.String");
                    }
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));
                    
				//定位到这里
                case TC_ARRAY:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an array to java.lang.String");
                    }
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an enum to java.lang.String");
                    }
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an object to java.lang.String");
                    }
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an exception to java.lang.String");
                    }
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
}

3、定位到 readOrdinaryObject(unshared) 这个方法中,checkResolve这个方法里面没有相关的代码,接下来定位到这一行,desc是我们序列化的类(单例类)


关键就在isInstantiable() 这个函数,这个函数的作用是检测一个类是够可以序列化,这里我们实现了Serializable 接口,所以是可以序列化的,那么这时候就会调用newInstance方法来进行创建新的类,再返回,这就是为什么我们拿到这个类的实例已经不是原来的实例了。

设计模式-单例模式(包括反射和序列化的影响和解决方法)_第3张图片



3. 解决方法

在《Effective Java》这本书中有说到,单例模式的序列化不仅仅是加上 implement Serializable是不够的,为了维护并保证单例,需要把每个实例域都声明为瞬时(transient),意味着不可实例化,并提供一个readResolve方法才可以保证。

public class LazySafe02 implements Serializable {

    private static volatile int count = 0;

    private LazySafe02() throws Exception {
        count ++;
        if(count > 1){
            throw new Exception("不能使用反射破坏单例");
        }
    }

    private volatile static LazySafe02 lazySafe02 = null;

    public static LazySafe02 getInstance() throws Exception {
        if(lazySafe02 == null){
            synchronized (LazySafe02.class) {
                if (lazySafe02 == null) {
                    lazySafe02 = new LazySafe02();
                }
            }
        }
        return lazySafe02;
    }

    //实现readResolve方法
    private Object readResolve() {
        return lazySafe02;
    }
}

在这里插入图片描述


结果很明显已经是同一个了,但是为什么呢?我们在单例类的LazySafe2里面的readResolve方法中加一个断点,我们调试来看看到底进入了哪个方法中
设计模式-单例模式(包括反射和序列化的影响和解决方法)_第4张图片

还是readOrdinaryObject类中, obj = desc.isInstantiable() ? desc.newInstance() : null 这行代码也运行了,但是到了下面的一段代码中就覆盖了。

//判断obj 是不是空,如果不是并且没有异常并且desc.hasReadResolveMethod() == true,意味着这个类里面实现了readResolve方法

  if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
        	//注意这里,调用了我们单例类中自己的readResolve方法,获取到了单例对象
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                //在这一行,把rep赋值给了obj,然后方法的最后把obj 进行了返回
                handles.setObject(passHandle, obj = rep);
            }
        }


其实说到底,底层代码中确实是新建了一个同样的对象,但是我们自己实现readResolve方法后又被覆盖,这就是为什么加了这一个方法就有用。



4. 枚举类的序列化

为什么枚举类就不会收到影响呢?我们还是回到源码看,这次把对象换成枚举类:下面代码是对枚举类的测试,可以看到返回的结果中枚举类的hashCode是相同的,证明是同一个对象

public enum SingletonEmun {
    INSTANCE;
      public static SingletonEmun getInstance(){
          return INSTANCE;
      }
}


public class TestSerializable {
    public static void main(String[] args) throws Exception {

        SingletonEmun before = SingletonEmun.getInstance();

        //我们把对象输出到一个文件上
        FileOutputStream fileOutputStream = new FileOutputStream("SingleTon.txt");
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(before);

        //再读取出来
        FileInputStream fileInputStream = new FileInputStream("SingleTon.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Object instance2 = objectInputStream.readObject();
        SingletonEmun after = (SingletonEmun)instance2;

        System.out.println(before.hashCode());
        System.out.println(after.hashCode());

        //1173230247
        //1173230247
    }
}



还是一样,通过源码来分析,这里我就直接给出方法的执行路径了,其实和上面的第3点这里的路径很类似

  1. private final Object readObject(Class type)
  2. Object obj = readObject0(type, false);
    这里方法里面是一个关键步骤,同样是switch-catch那里,第3点进入了TC_OBJECT,而这里则进入了TC_ENUM
	case TC_ENUM:
	     if (type == String.class) {
	              throw new ClassCastException("Cannot cast an enum to java.lang.String");
	          }
	     return checkResolve(readEnum(unshared));
  1. checkResolve(readEnum(unshared)) 中的 readEnum(unshared), 下面的代码中对一些关键信息作注释

  private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }
		//获取到枚举类的类,是ObjectStreamClass这种形式的
        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        //看看有没有ClassNotFoundException异常
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }
		//name是单例对象的名字,这里是INSTANCE
        String name = readString(false);
        Enum<?> result = null;
		//获取枚举类的类
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                //关键的方法在这
                //Enum.valueOf,等同于返回Enum.INSTANCE
                //用处就是返回枚举类中的一个对象
                Enum<?> en = Enum.valueOf((Class)cl, name);
                //对象赋值给result
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;	
        //最终返回给我们
        return result;
    }



5. 总结

其实通过上面的这些不难发现,反射和序列化都对枚举类做了单独的处理,所以如果想要实现单例,其实最简单的方法就是枚举。但是使用枚举也不一定有好处,《Effective Java》中提到,如果单例必须扩展一个超类,而不是扩展Enum的时候,不宜使用这个方法。在菜鸟教程中也给出了使用单例模式的推荐:

  • 一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式
  • 只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式
  • 如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式
  • 如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式





如有错误,欢迎指出

你可能感兴趣的:(设计模式,单例模式,java,开发语言,后端)