5.4 习题
5.4.1 balking 模式习题
希望 doInit() 方法仅被调用一次,下面的实现是否有问题,为什么?
public class TestVolatile { boolean initialized = false; public void init() { synchronized(this){ if (initialized) {//t2 return; } doInit();//t1 initialized = true; } } private void doInit() { } }
有问题,对initialized的读和写 多处出现。当t1在执行doInit()方法时,此时t2在第四行判断通过后也会执行doInit()方法,最终会导致doInit()被执行两次。
**解决办法:**参考同步模式之Balking
volitile适用于一个线程写多个线程读的情况,也可用于double-checked中synchronized外指令重排序问题
5.4.2 线程安全单例习题
单例模式有很多实现方法,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象(即调用getInstance)时的线程安全,并思考注释中的问题
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
实现1:饿汉式
/** * 问题1:为什么加 final? * 为了防止子类中的一些方法覆盖父类的提供单例的方法,从而破坏父类的单例 * 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 * 实现了序列化,就可以采用网络流的形式传输Java对象,最终可以通过反序列化创建对象。这个对象和单例模式创建的对象是不同的对象 * ,也就是破坏了单例。 * 解决办法:加一个public Object readResolve(){...} * 原因:在通过反序列化创建对象的过程中,如果发现readResolve()有返回值,则直接使用该方法,不再用反序列化的字节码来创建对象。 * * 问题3:为什么设置为私有? 是否能防止反射创建新的实例? * 如果为共有的话,也就可以直接new()无限的创建对象,也就不是单例了。 * 不能,暴力反射依旧可以创建实例对象 * * 问题4:这样初始化是否能保证单例对象创建时的线程安全? * 静态成员变量的赋值操作是在类加载阶段完成的,所以可以保证 * * * 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由 * ① 使用方法,可以有更好的封装性,也可以改进成懒惰初始化。 * ② 创建单例对象时可以有更多的控制 * ③ 可以对泛型进行支持 * */ 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; } }
实现2:枚举类实现单例
/** * 问题1:枚举单例是如何限制实例个数的 * 通过观察源码,可以发现枚举本质是一个静态成员变量,然后通过static{} * 进行初始化工作 * 问题2:枚举单例在创建时是否有并发问题 * 静态成员变量的赋值操作是在类加载阶段完成的,所以可以保证 * 问题3:枚举单例能否被反射破坏单例 * 不能,枚举类型不能通过newInstance反射。 * 问题4:枚举单例能否被反序列化破坏单例 * 不能,因为ENUM父类中的反序列化是通过valueOf实现的 不是通过反射 * 问题5:枚举单例属于懒汉式还是饿汉式 * 饿汉式 * 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做 * 可参考:https://blog.csdn.net/qq_39714944/article/details/91973832 * 注意:关于为啥强烈建议使用枚举类来实现单例模式的原因可参考: * https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg== * &mid=2650121482&idx=1&sn=e5b86797244d8879bbe9a69fb72641b5 * &chksm=f36bb82bc41c313d739f485383d3a868a79020c995ee86daef * 026a589f4782916c42a8d3f6c7&mpshare=1&scene=1&srcid=0614J9 * OX5zkoAnHiPYX2sHiH#rd */ //无参构造的枚举类 enum Singleton { INSTANCE; } //有参构造的枚举类 enum Singleton { INSTANCE(0,"INSTANCE"); int key; String value; //构造成员方法默认都是private,不能通过new的方式创建对象 Singleton(int key, String value) { this.key = key; this.value = value; } public int getKey() { return key; } public void setKey(int key) { this.key = key; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } }
实现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; } }
实现4:DCL ,本质就是对懒汉式的改进
- 缩小了synchronized的范围
- 但也带来了其他问题,比如指令可能重排序,可能重复创建对象。可通过双层判断 + volatile 解决
public final class Singleton { private Singleton() { } // 问题1:解释为什么要加 volatile ? // 为了防止指令的重排序。INSTANCE = new Singleton(); 对t1当发生重排序时候可能先进行 // 赋值操作,此时还没有进行实例化,这时候t2进来后 外部if (INSTANCE != null) 不为空,直接返回了INSTANCE,但是这个是还未初始化的实例 // 存在一定的问题。 //加上volatile后,就不会出现指令重排序了。要么当t2进行 外部if (INSTANCE != null) 时,已经初始化赋值完毕。要么t2进行if (INSTANCE != null) //时,此时INSTANCE为null,直接继续向下运行... 就不会出现问题了。 private static volatile Singleton INSTANCE = null; // 问题2:对比实现3, 说出这样做的意义 //缩小了锁的范围,第一次需要进入synchronized代码块,之后就不需要进入synchronized同步了。 public static Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗 //为了防止,第一次并发访问时,对象不要重复创建。比如t1正在 new 创建对象,此时t2在synchronized外等待,当t1 //t1创建完成并走出synchronized时,t2也会进入synchronized块,所以需要再次进行判断一下,防止重复创建。 if (INSTANCE != null) { // t2 return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } }
实现5:内部类的方式
public final class Singleton { private Singleton() { } // 问题1:属于懒汉式还是饿汉式 // 类加载本身就是懒惰的,第一次使用时才进行加载。 // 如果仅仅创建是使用外部的Singleton,没有使用getInstance(), // 就不会触发LazyHolder的类加载,也就不会进行静态变量的初始化操作。 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2:在创建时是否有并发问题 // 无 静态成员变量在类加载时进行初始化操作 public static Singleton getInstance() { return LazyHolder.INSTANCE; } }
本章小结
本章重点讲解了 JMM 中的
可见性 - 由 JVM 缓存优化引起
有序性 - 由 JVM 指令重排序优化引起
happens-before 规则
原理方面
CPU 指令并行
volatile
模式方面
两阶段终止模式的 volatile 改进
同步模式之 balking