安全的发布

四种方法

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到volatile类型域或者AtomicReference对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

    分析

  1. 不让其他程序new该类对象,咋办?——可以将类中的构造函数私有化。
  2. 在本类中new一个对象。
  3. 定义一个方法返回该对象,让其他程序可以获取到。之所以定义方法访问,就是为了可控。
  4. 要考虑指令重排序的情况

饿汉式单例模式

final成员方式

  1. public class Single
  2. {
  3. // 创建一个本类对象
  4. private static final Single s = new Single();
  5. // 构造函数私有化
  6. private Single(){}
  7. // 定义一个方法返回该对象,让其他程序可以获取到。之所以定义方法访问,就是为了可控
  8. public static Single getInstance()
  9. {
  10. return s;
  11. }
  12. }

静态块方式

错误方式

这个静态代码块先执行, 然后走到成员赋值, 被覆盖为null了, 对象是null

  1. import com.mmall.concurrency.annoations.ThreadSafe;
  2. /**
  3. * 饿汉模式
  4. * 单例实例在类装载时进行创建
  5. */
  6. public class SingletonExample6 {
  7. // 私有构造函数
  8. private SingletonExample6() {
  9. }
  10. static {
  11. instance = new SingletonExample6();
  12. }
  13. // 单例对象
  14. private static SingletonExample6 instance = null;
  15. // 静态的工厂方法
  16. public static SingletonExample6 getInstance() {
  17. return instance;
  18. }
  19. public static void main(String[] args) {
  20. System.out.println(getInstance().hashCode());
  21. System.out.println(getInstance().hashCode());
  22. }
  23. }

正确方式

  1. /**
  2. * 饿汉模式
  3. * 单例实例在类装载时进行创建
  4. */
  5. public class SingletonExample6 {
  6. // 私有构造函数
  7. private SingletonExample6() {
  8. }
  9. // 单例对象
  10. private static SingletonExample6 instance = null;
  11. static {
  12. instance = new SingletonExample6();
  13. }
  14. // 静态的工厂方法
  15. public static SingletonExample6 getInstance() {
  16. return instance;
  17. }
  18. public static void main(String[] args) {
  19. System.out.println(getInstance().hashCode());
  20. System.out.println(getInstance().hashCode());
  21. }
  22. }

懒汉式单例模式

双重检查锁

错误示例

双重检查
第一次检查时候,没有锁定,看这个有没有被实例化,有就直接返回
第二次检查,表面是没有初始化才第二次,锁定,初始化,返回

  1. public class Single {
  2. private static Single instance = null;
  3. private Single() {}
  4. public static Single getInstance() {
  5. if(instance == null) {
  6. synchronized(Single.class) {
  7. if(instance == null)
  8. instance = new Single();
  9. }
  10. }
  11. return instance;
  12. }
  13. }

我们先看 instance=new Single();
未被编译器优化的操作:

  1. 指令1:分配一款内存M
  2. 指令2:在内存M上初始化Singleton对象
  3. 指令3:将M的地址赋值给instance变量

编译器优化后的操作指令:

  1. 指令1:分配一块内存S
  2. 指令2:将M的地址赋值给instance变量
  3. 指令3:在内存M上初始化Singleton对象

现在有2个线程,刚好执行的代码被编译器优化过,过程如下:
image.png

正确示例

用volatile禁止重排序

  1. /**
  2. * 懒汉模式 -》 双重同步锁单例模式
  3. * 单例实例在第一次使用时进行创建
  4. */
  5. public class SingletonExample5 {
  6. // 私有构造函数
  7. private SingletonExample5() {
  8. }
  9. // 1、memory = allocate() 分配对象的内存空间
  10. // 2、ctorInstance() 初始化对象
  11. // 3、instance = memory 设置instance指向刚分配的内存
  12. // JVM和cpu优化,发生了指令重排
  13. // 1、memory = allocate() 分配对象的内存空间
  14. // 3、instance = memory 设置instance指向刚分配的内存
  15. // 2、ctorInstance() 初始化对象
  16. // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
  17. private volatile static SingletonExample5 instance = null;
  18. // 静态的工厂方法
  19. public static SingletonExample5 getInstance() {
  20. if (instance == null) { // 双重检测机制 // B
  21. synchronized (SingletonExample5.class) { // 同步锁
  22. if (instance == null) {
  23. instance = new SingletonExample5(); // A - 3
  24. }
  25. }
  26. }
  27. return instance;
  28. }
  29. }

静态内部类

由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序

  1. public class SingletonObject6 {
  2. private SingletonObject6(){}
  3. // 单例持有者
  4. private static class InstanceHolder{
  5. private final static SingletonObject6 instance = new SingletonObject6();
  6. }
  7. public final static SingletonObject6 getInstance(){
  8. // 调用内部类属性
  9. return InstanceHolder.instance;
  10. }
  11. }

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举模式

JVM 会阻止反射获取枚举类的私有构造方法

  1. /**
  2. * 枚举模式:最安全
  3. */
  4. public class SingletonExample7 {
  5. // 私有构造函数
  6. private SingletonExample7() {
  7. }
  8. public static SingletonExample7 getInstance() {
  9. return Singleton.INSTANCE.getInstance();
  10. }
  11. private enum Singleton {
  12. INSTANCE;
  13. private SingletonExample7 singleton;
  14. // JVM保证这个方法绝对只调用一次
  15. Singleton() {
  16. singleton = new SingletonExample7();
  17. }
  18. public SingletonExample7 getInstance() {
  19. return singleton;
  20. }
  21. }
  22. }

破坏单例模式的方法及解决办法

1、除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:

  1. private SingletonObject1() {
  2. if (instance != null) {
  3. throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
  4. }
  5. }

2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。

  1. public Object readResolve() throws ObjectStreamException {
  2. return instance;
  3. }

序列化打破单例和解决

  1. import java.io.Serializable;
  2. /**
  3. * 使用双重校验锁方式实现单例
  4. */
  5. public class Singleton implements Serializable{
  6. private volatile static Singleton singleton;
  7. private Singleton (){}
  8. public static Singleton getSingleton() {
  9. if (singleton == null) {
  10. synchronized (Singleton.class) {
  11. if (singleton == null) {
  12. singleton = new Singleton();
  13. }
  14. }
  15. }
  16. return singleton;
  17. }
  18. }

写一个序列化的测试类:

  1. import java.io.*;
  2. public class SerializableDemo1 {
  3. //为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
  4. //Exception直接抛出
  5. public static void main(String[] args) throws IOException, ClassNotFoundException {
  6. //Write Obj to file
  7. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
  8. oos.writeObject(Singleton.getSingleton());
  9. //Read Obj from file
  10. File file = new File("tempFile");
  11. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
  12. Singleton newInstance = (Singleton) ois.readObject();
  13. //判断是否是同一个对象
  14. System.out.println(newInstance == Singleton.getSingleton());
  15. }
  16. }

输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性

序列化破坏单例的过程

在反序列化的过程中

ObjectInputStream

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的。为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:
重点代码,readOrdinaryObject方法的代码片段:

  1. private Object readOrdinaryObject(boolean unshared)
  2. throws IOException
  3. {
  4. //此处省略部分代码
  5. Object obj;
  6. try {
  7. obj = desc.isInstantiable() ? desc.newInstance() : null;
  8. } catch (Exception ex) {
  9. throw (IOException) new InvalidClassException(
  10. desc.forClass().getName(),
  11. "unable to create instance").initCause(ex);
  12. }
  13. //此处省略部分代码
  14. if (obj != null &&
  15. handles.lookupException(passHandle) == null &&
  16. desc.hasReadResolveMethod())
  17. {
  18. Object rep = desc.invokeReadResolve(obj);
  19. if (unshared && rep.getClass().isArray()) {
  20. rep = cloneArray(rep);
  21. }
  22. if (rep != obj) {
  23. handles.setObject(passHandle, obj = rep);
  24. }
  25. }
  26. return obj;
  27. }
  1. Object obj;
  2. try {
  3. obj = desc.isInstantiable() ? desc.newInstance() : null;
  4. } catch (Exception ex) {
  5. throw (IOException) new InvalidClassException(
  6. desc.forClass().getName(),
  7. "unable to create instance").initCause(ex);
  8. }

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。 desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

这就解释了原因,为什么序列化会破坏单例?:序列化会通过反射调用无参数的构造方法创建一个新的对象。

防止序列化破坏单例模式

先给出解决方案,然后再具体分析原理:
只要在Singleton类中定义readResolve就可以解决该问题:

  1. import java.io.Serializable;
  2. /**
  3. * 使用双重校验锁方式实现单例
  4. */
  5. public class Singleton implements Serializable{
  6. private volatile static Singleton singleton;
  7. private Singleton (){}
  8. public static Singleton getSingleton() {
  9. if (singleton == null) {
  10. synchronized (Singleton.class) {
  11. if (singleton == null) {
  12. singleton = new Singleton();
  13. }
  14. }
  15. }
  16. return singleton;
  17. }
  18. private Object readResolve() {
  19. return singleton;
  20. }
  21. }

还是运行上面测试类:
本次输出结果为true。具体原理,我们回过头继续分析以下ObjectInputStream的readObject的调用代码:

  1. if (obj != null &&
  2. handles.lookupException(passHandle) == null &&
  3. desc.hasReadResolveMethod())
  4. {
  5. Object rep = desc.invokeReadResolve(obj);
  6. if (unshared && rep.getClass().isArray()) {
  7. rep = cloneArray(rep);
  8. }
  9. if (rep != obj) {
  10. handles.setObject(passHandle, obj = rep);
  11. }
  12. }

hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。