type
status
date
slug
summary
tags
category
titleIcon
password
icon
calloutIcon
📌
手写单例比较常见,单独列一篇记录所有写法 关键词:懒汉;Synchronized;多线程debug;双重检查;静态内部类;枚举;反编译;序列化;反射
笔记部分内容参考:

单例模式

基本概要

  • 类型:创建型
  • 目标:一个类仅有一个实例
  • 场景:
    • 共享计数器
    • 应用配置
  • 优点:
    • 节省内存
    • 方便控制访问(全局访问一个入口)
  • 缺点:
    • 不便于扩展
  • 实现要点:
    • private 构造
    • 并发安全
    • 懒加载
    • 序列化反序列化
    • 反射
  • 涉及内容:
    • 反编译
    • 内存可见性
    • 多线程debug

懒汉实现

特点

  • 延迟加载
  • 非线程安全
  • 可以加锁保证安全,但创建后后续读也会锁上
notion image

并发问题测试

  • 问题分析:当t1进入new时切换线程 t2也可以进入new
notion image
  • 多线程断点调试
  • 设置测试函数主线程断点,与单例构造里判断与赋值的断点(线程断点,Suspend选Thread)
notion image
notion image
  • 启动调试后观察到三个线程的RUNNING
notion image
  • 切换至Thread-1单步步入getInstance()
notion image
  • instance为null,正常步入new
notion image
  • 切换thread-2,同样正常步入new
notion image
  • 两边继续单步,t1得到2003
notion image
  • t2得到2004
notion image
  • LazySingleton被创建两次,如果在t1 print之前t2 完成new,那么最后t2赋值的为当前实例
notion image
  • 如果先卡住t2在new,令t1完成 再继续t2完成,会出现不同实例
notion image
notion image
notion image
  • 更换为synchronized方法后,调试看到t2被阻塞(MONITOR),并发问题解决
notion image
  • 重复测试 - 不上锁
notion image
notion image
  • 重复测试 - Synchronized锁getInstace方法
notion image
notion image

实现小结

  • synchronized写方法体上会使得即使创建实例之后每次读取也会加锁
  • 更高效的实现应写入方法体中,已有实例不再加锁,直接返回
  • 没有实例先拿锁,如果等到拿到锁的时候还没有再创建
 

改进懒汉

引入

  • 相比于在方法体上上锁使得后续读被阻塞,可以在方法内部上锁,每次先进行判断,非空就不再上锁
  • 但new LazySingleton()创建操作与instance =的赋值操作并不是原子的,可能发生指令重排序引起并发安全问题
notion image
  • 所以实现上还需要补充volatile禁止重排序(初始化与设置字段值可以在单线程中重排序,不影响执行结果)

实现与测试

  • 需要两次检查,外边避开后续读上锁,里面防止除了第一个拿到锁的其他等待锁的线程重复创建
notion image
notion image
notion image

静态内部类

引入

  • 只有首次访问静态内部类时会触发其类初始化,通过JVM的Class对象初始化锁(可以理解为上方的在t1创建完之前t2阻塞访问),此时重排序对t2不可见(类初始化-创建实例/静态成员访问[字段赋值,方法调用]触发)
notion image

实现与测试

notion image
notion image
notion image
  • 只要不调用getInstance,是不会触发子类构造的,所以也是懒加载
notion image
notion image
注:如果在IDEA里调试时,将鼠标放在了instance上,是会触发懒加载的,以下是同一函数的调用,结果不一样,区别只在于是否将鼠标放置在instance上
notion image
notion image
notion image

饿汉实现

类加载时完成初始化 避免线程同步问题 没有延迟加载,不使用造成浪费
notion image

序列化保证单例

问题引入

  • 不做处理的情况
notion image
notion image
  • 定位到输入流类里readObject,进一步步入到readOrdinaryObject观察到通过反射创建了新实例
notion image

不正确的解决方法

  • 创建新实例之后检查是否有readResolve方法,有就调用并用其返回值覆盖新建的对象
notion image
notion image
  • 字符串写死的方法名”readResolve”
notion image
notion image
notion image
  • 可以自行写readResolve方法,覆盖读取时新建的结果
notion image
notion image

反射保证单例

问题引入

  • 不处理的情况
notion image
notion image

问题解决

  • 类构造函数中检查,禁止重复创建(只使用饿汉|静态内部类,初始创建已经赋值instance,且只可用于防反射)
notion image
  • 用于防止反射(反射调构造器newInstance)时重复
notion image
注:懒汉反射创建的是不会更新字段里的数据的,所以先反射再getInstance会出现两个实例,防不住
notion image
notion image
notion image
notion image
notion image
思考:带参构造是否能解决?
  • 注:构造函数检查的方法防不住序列化,因为序列化并没有调用对应类的构造函数
  • 参考以下情况,去除掉readResolve,如果调用了构造函数,那么测试应该看到IllegalStateException
notion image
  • 但实际却是AssertionError,意味着readObject正常返回了,没有被构造函数异常阻止
notion image
  • 重新回到源码,到断点为止,虽然读取了HungrySingleton的类描述信息,但是里面给到的构造器是Object的构造器,且这instance方法并不是反射测试里的构造器类型(Constructor),是ObjectStreamClass类上的
notion image
  • 里面主要内容就是调用Object类构造器
notion image
notion image
notion image
  • 可以看到Constructor里this为Object
notion image
  • 调用对象ca的newInstance后直接转成了目标类型
notion image
  • 然后进行序列化数据读取
notion image
  • 实验下来就是反序列化创建了对应类的对象但没有调用对应类的构造函数

枚举Enum单例(推荐)

功能测试

  • 并发安全 - JVM负责创建
  • 序列化
notion image
  • 带数据测试
notion image
notion image

序列化

  • 序列化时调用valueOf直接从常量字典读取,没有新对象创建的过程,避免序列化破坏单例
notion image
notion image

反射

  • 枚举没有空参构造,直接获取会异常
notion image
notion image
  • 如果按照对应参数定义获取,会被newInstance方法中判断拦截抛异常
notion image
notion image

反编译结果

  • JAD反编译
  • 可以看到枚举的实现
    • final class无法被继承
    • 继承Enum类
    • 实例为public static final,在static代码块完成初始化
notion image
notion image
 
体验MCP灵茶山艾府力扣题单follow
Loading...
CamelliaV
CamelliaV
Java;CV;ACGN
最新发布
单例模式的四种写法
2025-4-24
体验MCP
2025-4-24
MetingJS使用自定义音乐源-CF+Huggingface部署
2025-4-2
博客访问站点测速分析与对比
2025-3-26
前端模块化
2025-3-16
Voxel2Mesh相关论文精读与代码复现
2025-3-15
公告
计划:
  • LLM相关
  • 支付业务 & 双token无感刷新
  • (线程池计算优惠方案)天机学堂Day09-Day12复盘-优惠劵业务
  • (业务复盘,技术汇总)天机学堂完结复盘
  • hot 100
 
2024-2025CamelliaV.

CamelliaV | Java;CV;ACGN