单例形式
懒汉模式
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对象
- 解决
- 利用关键字 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());
}
}
com.rbac.console.singleton.LazySingleTon@2d38eb89
com.rbac.console.singleton.LazySingleTon@cc34f4d
false
- 原因分析
- 从ois.readObject()这个方法为入口,即ObjectInputStream类的readObject方法找到readObject0方法中的switch片段,判断反序列化对象类型,此时对象类型是Object
- 找到readObject0方法中的switch片段,判断反序列化对象类型,此时对象类型是Object
- 返回值会调用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方法,并将获得的对象返回,所以序列化前后对象相同!阻止了单例被破坏