Java 设计模式之单例模式

259 阅读2分钟

单例模式有饿汉式和懒汉式,这里只说懒汉式(面试总爱问到)

Java单例的三种经典实现

1、枚举

public enum EnumSingleton {
    INSTANCE;
    public void talk() {
        System.out.println("This is an EnumSingleton " + this.hashCode());
    }
}

枚举内每一个成员变量都是每个枚举自身的实例,每一个枚举都继承自java.lang.Enum类,枚举的每个成员默认都是 public static final 的。

总结:枚举实现单例是最简单也是最安全的。后面会说到

2、静态内部类

public class StaticInnerClass {
    // 私有的构造器
    private StaticInnerClass() {
    }
    //获取实例
    public static StaticInnerClass instance(){
        // 饿汉式进行初始化
        return InnerClass.instance;
    }
    //静态内部类  JVM初始化加载时候,是线程安全的
    private static class InnerClass{
        // 当内部类加载时候,初始化成员变量
        private static StaticInnerClass instance = new StaticInnerClass();
    }
}

这里附上类加载顺序

3、双重检查锁(DCL)

/*
    并发编程的三大特性:原子性、可见性、有序性
 */
public class DCL {
    private DCL() {
    }
    // volatile 保证可见性和有序性
    private static volatile DCL instance = null;
    public static DCL getInstance(){
        if (instance != null) return instance;
        //synchronized 保证原子性
        synchronized (DCL.class){
            if (instance == null) {
                /*
                    volatile有序性的作用
                    new DCL() 在jvm中解析为
                    1、创建实例 在堆中开辟空间
                    2、属性初始化
                    3、将对象的内存地址赋值给instance
                    第二步和第三步没有必然关联性,顺序可能颠倒
                    (jvm为了提升性能,编写的代码可能喝执行的顺序不一致、反生了指令重排序)
                 */
                /*
                    volatile可见性的作用(前提条件 多CPU情况下)
                    可见性是由于 内存和cup(高速缓存 )交互产生的问题
                    通过MESI协议解决  关于MESI协议后续更新
                 */
                instance = new DCL();
            }
        }
        return instance;
    }
}

破坏单例

1、反射攻击

		// 通过反射,获取单例类的私有构造器
		Constructor constructor = DCL.class.getDeclaredConstructor();
		// 设置私有成员的暴力破解
		constructor.setAccessible(true);
		// 通过反射去创建单例类的多个不同的实例
		DCL s1 = (DCL) constructor.newInstance();
		// 通过反射去创建单例类的多个不同的实例
		DCL s2 = (DCL) constructor.newInstance();
		s1.tellEveryone();
		s2.tellEveryone();
		System.out.println(s1 == s2);

2、序列化攻击

		// 对象序列化流去对对象进行操作
		ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("file"));
		// 通过单例代码获取一个对象
		DCL s1 = DCL.getSingletonInstance();
		// 将单例对象,通过序列化流,序列化到文件中
		outputStream.writeObject(s1);
		// 通过序列化流,将文件中序列化的对象信息读取到内存中
		ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(new File("file")));
		// 通过序列化流,去创建对象
		DCL s2 = (DCL) inputStream.readObject();
		s1.tellEveryone();
		s2.tellEveryone();
		System.out.println(s1 == s2);

总结:以上两种攻击对枚举是无效的,枚举不能通过反射得到对象,枚举不能序列化得到对象,都会报错