设计模式之单例模式终极版【克隆-序列化-反射】

1 基本内容

1.1 概念

单例模式,是指在任何时候,该类只能被实例化一次,在任何时候,访问该类的对象,对象都是同一个。只要是程序员都会使用到,甚至都不能算是设计模式。但是在我们使用中也需要了解一下单例特性和使用场景

1.2 模式优缺点

单例模式有以下优点: >使用单例模式可以严格的控制用户怎样以及如何访问它 节约系统资源,提高系统的性能

单例模式有以下缺点: >不易扩展 单例类职责过重,在一定程度上违背了“单一职责原则” 如实例化对象长时间未使用,会GC回收,导致对象状态的丢失

# 2 单例模式分类

2.1 饿汉模式


public class SingletonEHan {

    private SingletonEHan() {}

    /**
     * 1.单例模式的饿汉式
     */
    private static SingletonEHan singletonEHan = new SingletonEHan();

    public static SingletonEHan getInstance() {
        return singletonEHan;
    }

//     SingletonEHan instance= SingletonEHan.getInstance();

    /**
     * 2. 单例模式的饿汉式变换写法
     * 基本没区别
     */
    private static SingletonEHan singletonEHanTwo = null;

    static {
        singletonEHanTwo = new SingletonEHan();
    }

    public static SingletonEHan getSingletonEHan() {
        if (singletonEHanTwo == null) {
            singletonEHanTwo = new SingletonEHan();
        }
        return singletonEHanTwo;
    }
    //     SingletonEHan instance= SingletonEHan.getSingletonEHan();


}
  • 优点:在类加载的时候就完成了实例化,避免了线程的同步问题
    • 缺点:由于在类加载的时候就实例化了,未懒加载,会造成内存的浪费

2.2 懒汉模式

2.2.1 DCL双重校验锁


public class Singleton {

    private volatile static Singleton singleton;

    public static Singleton getInstance() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}
  • 优点:线程安全,延迟加载
  • 缺点:使用了synchronized关键字,需要同步获取,影响性能

2.2.2 静态内部类

:::java
public class SingletonIn implements Serializable {

    private static final long serialVersionUID = -2424536714640756316L;

    private SingletonIn() {
    }

    private static class SingletonInHolder {
        private static final SingletonIn singletonIn = new SingletonIn();
    }

    public static SingletonIn getSingletonIn() {
        return SingletonInHolder.singletonIn;
    }

    public static void main(String[] args)  {
        SingletonIn singleton = getSingletonIn();
    }
}
  • 优点:静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全的,延迟加载,效率高,推荐使用

2.3 枚举

:::java
public enum SingletonEnum {

    INSTANCE(1,"i am singleton");

    private int code;

    private String message;
    
    SingletonEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public final int getCode() {
        return code;
    }
    
    public final String getMessage() {
        return message;
    }
}

大佬喜欢的用法,简单粗暴,其实有很多学问在里面,请看下文就会知道

3 克隆-序列化-反射对单例的影响

对象的创建方式有哪几种? 四种:new 、克隆、序列化、反射 上面的单例模式使用的都是new创建对象,那么其他三种方式对单例是否有影响呢?

3.1 克隆对单例的影响

实现Cloneable 接口,尽管构造函数是私有,但还会创建一个对象。因为clone方法不会调用构造函数,会直接从内存中copy内存区域。所以单例模式的类是不可以实现Cloneable接口

以静态内部类的单例方式做测试

public static void main(String[] args) throws CloneNotSupportedException {
        SingletonIn singleton = getSingletonIn();
        SingletonIn singleton1 = (SingletonIn) singleton.clone();
        SingletonIn singleton2 = getSingletonIn();
        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
    }

测试结果

1173230247
856419764
1173230247

hash值不一样,所以克隆成功了,生成了一个新对象,所以当对象的使用要求单例的时候,切记不可实现*cloneable*接口

3.2 序列化对单例的影响

3.2.1 静态内部类单例反序列化 如下方式,我们通过序列化反序列化获取对象,观察对象是否还是单例对象

public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingletonIn singleton = getSingletonIn();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\single.obj"));
        oos.writeObject(singleton);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\single.obj")));
        SingletonIn singleton1 = (SingletonIn) ois.readObject();
        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(singleton == singleton1);
    }

控制台打印结果如下:

312714112
692404036
false

hash值不一样,获取的对象非同一对象

3.2.2 枚举序列化反序列化

public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingletonEnum singleton = SingletonEnum.INSTANCE;
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\single.obj"));
        oos.writeObject(singleton);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\single.obj")));
        SingletonEnum singleton1 = (SingletonEnum) ois.readObject();
        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(singleton == singleton1);
    }

打印结果

2125039532
2125039532
true

枚举方式,即使序列化反序列化也不会破坏单例

3.2.3 防止序列化对单例的破坏 自定义实现对象的readResolve()方法可以解决问题

:::java
 private Object readResolve() {
        return getSingletonIn();
    }

打印结果

312714112
312714112
true

这样反序列化获取到的就是原有的单例了

3.2.4 原理分析 debug反序列化方法readObject(),调用链如下: 调用链

主要分析这部分1767行开始 readOrdinaryObject 下面的源码

//获取SingletonIn的Class
 Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
               || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {//如果所代表的类是可序列化或者自定义序列化并且可以序列化runtime时候被实例化,则返回true
            obj = desc.isInstantiable() ? desc.newInstance() : null;
      //debug在这里通过反射创建了对象
        } catch (Exception ex) {
            //省略
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }
      //是否实现了自定义序列化接口
        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
      //读取持久化的数据并写入对象
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null & handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
       //如果所代表对象实现了可序列化或者自定义序列化接口,并且定义了readResolve方法,则返回true
        {
            //获取readResolve方法中的对象,并替换obj
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                handles.setObject(passHandle, obj = rep);
            }
        }
        //返回返序列化对象
        return obj;

从上面源码可知,反序列化的时候,通过反射会创建新的对象,所以不是单例,如果类实现了serializable or externalizable 接口,定义readResolve方法,可以控制最终反序列对象,所以重写该方法,可以实现单例

而如果是枚举类型的话主要关注checkResolve(readEnum(unshared)) 方法,readEnum(unshared)返回的对象就是INSTANCE对象

//获取枚举类对象
 private Enum<?> readEnum(boolean unshared) throws IOException {
   
      //分配给给定对象的下一个可用句柄,并分配赋值处理,
        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        //获取枚举类Class
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try{
                //方法返回具有指定名称的枚举类型的枚举常量,获取到INSTANCE对象
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
             // 省略
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

private Object checkResolve(Object obj) throws IOException {
       //在这一步就返回对象了
        if (!enableResolve || handles.lookupException(passHandle) != null) {
            return obj;
        }
        Object rep = resolveObject(obj);
        if (rep != obj) {
            handles.setObject(passHandle, rep);
        }
        return rep;
    }

魔法在java.lang.Enum.valueOf()中,最后获取的是常量类

3.3 反射对单例的影响

:::java
    public static void main(String[] args)
            throws ReflectiveOperationException {
        Class cls = SingletonIn.class;
        Constructor<SingletonIn> constructor = cls.getDeclaredConstructor();
        constructor.setAccessible(true);
        SingletonIn singleton = constructor.newInstance();
        SingletonIn singleton1 = getSingletonIn();
        System.out.println(singleton.hashCode());
        System.out.println(singleton1.hashCode());
        System.out.println(singleton == singleton1);
    }

打印结果

1173230247
856419764
false

反射直接破坏单例,解决方法是在构造器使用同步块互斥

    private static volatile boolean init = false;

    private SingletonIn() {
        synchronized (Singleton.class) {
            if (init) {
                throw new RuntimeException("只能单例获取");
            }
            init = !init;
        }
    }

在这种条件下,再次通过反射调用就会报错:

Exception in thread "main" java.lang.ExceptionInInitializerError
	at designpattern1.singleton.inclass.SingletonIn.getSingletonIn(SingletonIn.java:43)
	at designpattern1.singleton.inclass.SingletonIn.main(SingletonIn.java:57)
Caused by: java.lang.RuntimeException: 只能单例获取

4 总结

枚举类型是绝对单例的,可以无责任使用 其他需根据场景使用不同版本,在有序列化和反射的场景下,选择合适的安全版本

文章目录
-----------------------
最新评论

[评论][COMMENTS]