黑马程序员《深入学习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 保证,没有问题
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器