Java 设计模式 —— 单例模式

2250

单例形式

懒汉模式

public class SingleDemo {
    private static SingleDemo instance;

    private SingleDemo() {}

    public SingleDemo getInstance() {
        if (instance == null) {
            instance = new SingleDemo();
        }

        return instance;
    }
}

饥饿模式

public class SingleDemo {
	private static SingleDemo instance = new SingleDemo();
	
	private SingleDemo() {}
	
	public SingleDemo getInstance() {
		return instance;
	}
}

枚举式

public enum EnumSingleDemo {
    INSTANCE;
    public  void method() {
        // do work.
    }
}

存在问题

懒汉式为什么是线程不安全的

  • 单例模式有两种实现方式,即大家所熟悉的饿汉式和懒汉式。二者的区别是创建实例的时机,饿汉式在应用启动时就创建了 实例,饿汉式是线程安全的,是绝对单例的。懒汉式在对外提供的获取方法被调用时会实例化对象。在多线程情况下,懒汉模式不是线程安全的

JVM 编译器的 指令重排 对 懒汉模式 单例的影响

  • 指令重排
    • Singleton instance = new Singleton()会被编译器编译成如下 JVM 指令
memory = allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance = memory;     //3:设置instance指向刚分配的内存地址
- 指令顺序并非一成不变,有可能会经过 JVM 和 CPU 的优化,指令重排成下面的顺序
memory = allocate();    //1:分配对象的内存空间 
instance = memory;     //3:设置instance指向刚分配的内存地址 
ctorInstance(memory);  //2:初始化对象
  • 影响
    • 当线程 A 执行完1,3,时,准备走2,即 instance 对象还未完成初始化,但已经不再指向 null
    • 此时如果线程 B 抢占到CPU资源,执行 if(instance == null)的结果会是 false
    • 从而返回一个没有初始化完成的instance对象

1635498173665-333862f8-6600-45bb-8202-b58fffd10268

  • 解决
    • 利用关键字 volatile 来修饰 instance 对象,阻止变量访问前后的指令重排,从而保证了指令的执行顺序

序列化、反序列化对单例的破坏

  • 序列化意义是将实现序列化的Java对象转换成字节序列,这些字节序列可以被保存在磁盘上,或者通过网络传输。以备以后重新恢复成原来的对象。
  • 对于单例类使用序列化、反序列化操作时,会破坏单例(序列化前的对象和反序列化后得到的对象内存地址不同)
import java.io.*;
 
public class LazySingleTon implements Serializable {
    private LazySingleTon(){
    }
    public static  LazySingleTon getInstance(){
        return  InnerClass.lazySingleTon;
    }
    private static class InnerClass{
        private  static LazySingleTon lazySingleTon = new LazySingleTon();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
        oos.writeObject(LazySingleTon.getInstance());
        File file = new File("tempFile");
        ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
        LazySingleTon newInstance = (LazySingleTon) ois.readObject();
        //判断是否是同一个对象
        System.out.println(newInstance);
        System.out.println(LazySingleTon.getInstance());
        System.out.println(newInstance == LazySingleTon.getInstance());
    }
 }
  • w
    • 运行结果
com.rbac.console.singleton.LazySingleTon@2d38eb89
com.rbac.console.singleton.LazySingleTon@cc34f4d
false
  • 原因分析
    • 从ois.readObject()这个方法为入口,即ObjectInputStream类的readObject方法找到readObject0方法中的switch片段,判断反序列化对象类型,此时对象类型是Object

16365262820569946750e5d9a4dc696ddc47a26a6ce0f.png

- 找到readObject0方法中的switch片段,判断反序列化对象类型,此时对象类型是Object

16358490083758855941de0474dd9b39cdd047e5c8655.png

- 返回值会调用readOrdinaryObject方法,readOrdinaryObject方法中的三目允许算符判断了对象是不是可实例化的,如果是可实例化的会通过newInstance()方法反射实例化一个新的对象,所以序列化前的对象和反序列化后得到的对象不同
  • 解决方案
    • 解决方案是在单例类中加一个readResolve方法
public class LazySingleTon implements Serializable {
    //其他方法,略

    /**
         * 解决序列化、反序列化破坏单例
         * @return
         */
    public Object readResolve(){
        return getInstance();
    }
}
- 输出
com.rbac.console.singleton.LazySingleTon@cc34f4d
com.rbac.console.singleton.LazySingleTon@cc34f4d
true
- 可以看到这次序列化前后对象一致,单例没有被破坏
  • 原因
    • 在刚才分析的readOrdinaryObject方法有调用hasReadResolveMethod的判断,这个方法是验证目标类是否包含一个方法名为readResolve的方法,如果有就执行desc.invokeReadResolve,通过反射调用单例类的LazySingleTon的readResolve方法,即我们刚才加的readResolve方法,并将获得的对象返回,所以序列化前后对象相同!阻止了单例被破坏

16358491837327affe2288dd04fc19ee6775f5515bb6a.png