单例模式复习

单例模式,是java的基本设计模式之一,即全局最多只创建一个实例,实现的主要方法是私有构造方法和提供获取实例的方法

1.实现方式

常用的实现方式有饿汉式,懒汉式(DCL),静态内部类,枚举类

1.1 饿汉式

饿汉式即不管用不用到,先创建一个实例,放在内存中,然后提供给外界访问.这样做的好处是免去了诸多麻烦,但也带来了不必要的内存开销

public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
    private Singleton1(){}
}

1.2 懒汉式1

为了优化饿汉式的内存开销,使用延迟加载实例,在需要时再构建

public class Singleton2 {
    private static Singleton2 INSTANCE = null;
    public static Singleton2 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }
    private Singleton2(){}
}

1.3 懒汉式2(双重校验锁(Double Check Lock,DCL))

由于懒汉式在多线程下,可能出现创建多个实例的情况,因此需要针对多线程加上synchronized关键字保证相关代码线程安全(可以直接加到方法修饰符),但是线程安全的代码在运行是十分耗时,因此只在第一次获取实例经过线程安全代码即可,第一个判断是为了避免每次获取实例都经过线程安全的代码,第二次判断才是判断实例有没有被创建,这样保证了线程安全延迟加载,这种设计被称为双重校验锁

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

1.4 懒汉式3(DCL完善)

上面的双重校验锁看似已经完美的节省了时间和空间的开销,但是却存在另一个问题,即Jvm运行中存在着乱序执行的机制,在创建实例时,在jvm里面的执行分为三步:
1. 在堆内存开辟内存空间。
2. 在堆内存中实例化SingleTon里面的各个参数。
3. 把对象指向堆内存空间。
由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。
不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile关键字,即在JDK1.6及以后,只要定义为private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

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

    private Singleton4() {}
}

到此为止,懒汉式看似已经无懈可击,但其实还有其他问题,在下面会提到

1.5 静态内部类

静态内部类的特点是,不会随着外部类加载,只有当被引用到才会加载,因此在静态内部类中放一个外部类的实例,就刚好能实现实例的延迟构建,而且也没有多线程问题(静态内部类的多线程安全问题请参考下方链接)
但这种也有明显的缺点,即只适用于无参构造器

public class Singleton5 {
    public static Singleton5 getInstance() {
        return Inner.INSTANCE;
    }
    private Singleton5() {
    }

    private static class Inner {
        private static final Singleton5 INSTANCE = new Singleton5();
    }
}

1.6 枚举类

枚举类构造函数默认为private,而且可以公开声明自己的实例,所以天生就符合实现单例的条件,而且可以有效避免破解单例的几种方法(下面提到),还能声明方法,就安全性和便捷来说,是实现单例的完美方法

public enum Singleton6 {
    SINGLETON
    ;

    Singleton6(){}

	public void doSomething(){}
}

2.单例的破解方法和应对手段

虽然单例有很多种实现方案,但奈何道高一尺,魔高一丈,一个反射就干死一片,还有序列化和克隆都会使单例失效

2.1 反射

反射可以拿到构造方法,然后暴力破解,使单例失效

private static void showReflectDestroySingleton() {
   try {
        Class<?> aClass = Singleton4.class;
        Constructor<?> constructor = aClass.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        System.out.println(constructor.newInstance());
        
        Method getInstance = aClass.getDeclaredMethod("getInstance");
        Object instance1 = getInstance.invoke(null);
        System.out.println(instance1);
    } catch (Exception e) {
        e.printStackTrace();
    }
}


这种破解会使饿汉式和懒汉式以及静态内部类实现的单例失效,于是,我们尝试以同样的方式破解枚举类:

private static void showReflectDestroySingleton1() {
    try {
        Class<?> aClass = Singleton6.class;
        Field field = aClass.getField("SINGLETON");
        System.out.println(field.get(null));

        Constructor<?> constructor = aClass.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        Object instance = constructor.newInstance();
        System.out.println(instance);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

结果,构造函数调用失败,查看源码可以看到反射机制排除了枚举这一类型! 牛批,你有降龙锏,我有伏凤针!
在这里插入图片描述
在这里插入图片描述

2.2 序列化

当一个单例刚好需要序列化时,需要实现Serializable接口,反序列化时会"组装"一个实例,这个过程是不经过构造器的,这种时候单例就会遭到破坏

private static void showSerializationDestorySingleton() {
 	File file = new File("C:\\Users\\Admin\\Desktop\\demo.txt");
    try (
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    ) {
        Singleton4 instance1 = Singleton4.getInstance();
        out.writeObject(instance1);

        System.out.println(instance1);
        System.out.println(in.readObject());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这里插入图片描述
这种时候就可以往单例中添加一个readResolve方法,指定获取实例的方式,就可以避免单例被破坏

public class Singleton4 implements Serializable{
    private static volatile Singleton4 INSTANCE = null;

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

    private Singleton4() throws Exception {
        System.out.println("Singleton4: 构造函数执行...");
    }

    /**
     * 防止反序列化破坏单例,反序列化时,如果定义了readResolve方法,则直接返回方法指定的对象,而不需要单独再创建对象
     * https://blog.csdn.net/weixin_42130471/article/details/89602999
     */
    private Object readResolve() throws ObjectStreamException {
        return getInstance();
    }
}

然后在进行序列化测试,发现没有创建新的实例:
在这里插入图片描述
而对于枚举类,其超类Enum中已经实现了Serializable接口,而且ObjectInputStream中也提供了针对枚举类的读取方法,会根据枚举的name字段获取到对应实例,因此也不会产生额外的实例
在这里插入图片描述

2.3 克隆

如果一个类需要用到克隆,就需要实现Cloneable接口,并复写clone方法,然后就可以克隆实例.

@Override
protected Object clone() throws CloneNotSupportedException {
    return super.clone();
}
@Override
private static void showCloneableDestorySingleton(){
  try {
        Singleton4 instance = Singleton4.getInstance();
        System.out.println(instance);
        System.out.println(instance.clone());
    }catch (Exception e){
        e.printStackTrace();
    }
}

但这个操作也没有有经过构造器,而且单例依然遭到破坏
在这里插入图片描述
查看Object中的clone方法发现是一个native方法
在这里插入图片描述
其实这个过程是直接在内存中对实例进行拷贝,跳过了构造器
而枚举类已经完全绕过了克隆,因为在超类Enum中已经复写了Object中的clone方法,并且声明为protected和final,其子类无法复写和调用,就算被调用也会触发异常,太狠了!
在这里插入图片描述
因此,单例要避免实现Cloneable接口和复写clone方法,而枚举则不需要考虑这种情况

综上可见,枚举是单例的最佳实现,但实际应用还是要看情况而定

参考链接:
https://blog.csdn.net/mnb65482/article/details/80458571.
https://blog.csdn.net/chao_19/article/details/51112962.
https://www.jianshu.com/p/d9d9dcf23359.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值