黑马程序员《深入学习Java并发编程》笔记

 

单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用 getInstance)时的线程安全,并思考注释中的问题

  • 饿汉式类加载就会导致该单实例对象被创建
  • 懒汉式类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

 

实现1(静态成员变量,饿汉式)

// 问题1:为什么加 final
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
   // 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
   private Singleton() {}
   // 问题4:这样初始化是否能保证单例对象创建时的线程安全?
   private static final Singleton INSTANCE = new Singleton();
   // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
   public static Singleton getInstance() {
     return INSTANCE;
   }
   public Object readResolve() {
     return INSTANCE;
   }
}

问题1:为什么加 final

怕有子类,子类里不适当地覆盖了父类地方法,破坏了单例

问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例(不止能用 new 创建对象,如果实现了序列化接口,反序列化也会创建对象

加一个方法,反序列化时发现如果有 readResolve() 那么会把这个方法返回的对象当作反序列化对象,而不真正反序列化

public Object readResolve() {
 return INSTANCE;
 }

问题3:为什么设置为私有? 是否能防止反射创建新的实例?

为了防止其它类调用构造方法创建对象。但是这样不能防止反射创建新的实例。

问题4:这样初始化是否能保证单例对象创建时的线程安全?

可以,静态变量是类加载时候初始化的,由 JVM 保证它的线程安全

问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由

提供更好的封装性:便于以后实现懒惰初始化;可以提供泛型的支持;可以防止其它类修改 instance

 

实现2(枚举类,饿汉式)

enum Singleton { 
 INSTANCE; 
}

问题1:枚举单例是如何限制实例个数的

从反编译结果可以看出,INSTANCE 就是枚举类 Singleton 的一个静态成员变量,所以是单实例的。

问题2:枚举单例在创建时是否有并发问题

静态成员变量,由 JVM 在类加载时完成创建,无并发问题

问题3:枚举单例能否被反射破坏单例

可以

问题4:枚举单例能否被反序列化破坏单例

枚举类默认都是实现了序列化接口。而且考虑到了反序列化破坏单例的情况,不用自己做任何操作。

问题5:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做

可以在枚举类里加一个构造方法

 

实现3(整个方法加锁,锁住类对象,懒汉式)

public final class Singleton {
   private Singleton() { }
   private static Singleton INSTANCE = null;
   // 分析这里的线程安全, 并说明有什么缺点
   public static synchronized Singleton getInstance() {
   if( INSTANCE != null ){
     return INSTANCE;
   } 
   INSTANCE = new Singleton();
   return INSTANCE;
 }
}

可以保证线程安全,类对象与 instance 一一对象。注意 sychronized 不能加在 instance 上,因为 instance 可能为 null

缺点就是锁的范围有点大,即时对象已经创建好了,后续获取instance时还是要加锁,性能较低

 

实现4(双重锁检查,锁类对象,懒汉式)

public final class Singleton {
   private Singleton() { }
   // 问题1:解释为什么要加 volatile ?
   private static volatile Singleton INSTANCE = null;
 
   // 问题2:对比实现3, 说出这样做的意义 
   public static Singleton getInstance() {
     if (INSTANCE != null) { 
       return INSTANCE;
     }
     synchronized (Singleton.class) { 
       // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
       if (INSTANCE != null) { // t2 
         return INSTANCE;
       }
       INSTANCE = new Singleton(); 
       return INSTANCE;
     } 
   }
}

问题2:对比实现3, 说出这样做的意义

对于已经初始化好了 instance 时,后续获取时不用再加锁,性能较高

问题3:为什么还要在这里加为空判断, 之前不是判断过了吗

防止首次创建 instance 时,多个线程并发的问题:t1 在执行初始化,t2 在 sychronized 处等待。t1执行完了,t2进来,如果没有 sychronized 里面的判断,t2 就又会执行一次初始化

问题1:解释为什么要加 volatile ?

对于 INSTANCE = new Singleton();这一句, jvm 可能会优化为:先执行 赋值操作,再 调用构造方法。如果两个线程 t1,t2 按如下时间序列执行:

上图:

  • t1 线程执行 INSTANCE = new Singleton(); 这一句时指令重排序了,也就是先赋值再调用构造方法。
  • t2 线程 在t1赋值完还没调用初始化之前,在这个间隙正在执行外层的 if (INSTANCE == null) ,因为t1已经赋值了,不为 null ,所以直接返回了没有初始化的 instance,若 t2 再紧接着使用这个没有初始化的 instance ,就会出现问题

getstatic 这行代码(第一个外层的那个非null判断)在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值

这时 t1 只是被赋值了, 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

 

实现5(静态内部类,懒汉式)

public final class Singleton {
   private Singleton() { }
   // 问题1:属于懒汉式还是饿汉式
   private static class LazyHolder {
     static final Singleton INSTANCE = new Singleton();
   }
   // 问题2:在创建时是否有并发问题
   public static Singleton getInstance() {
     return LazyHolder.INSTANCE;
   }
}

问题1:属于懒汉式还是饿汉式?

懒汉式。类加载本身就是懒惰的,类只有第一次被用到的时候才执行类加载操作。如果只是用外部类 Singleton,而没有调用 getInstance 去使用内部类 LazyHolder ,就不会触发 LazyHolder 的类加载,它里面的静态变量也就不会执行初始化操作(静态变量初始化在类加载阶段)

问题2:在创建时是否有并发问题?

类加载时对静态变量的赋值操作的线程安全性由 JVM 保证,没有问题