type
status
date
slug
summary
tags
category
titleIcon
password
icon
calloutIcon
手写单例比较常见,单独列一篇记录所有写法
关键词:懒汉;Synchronized;多线程debug;双重检查;静态内部类;枚举;反编译;序列化;反射
笔记部分内容参考:
单例模式
基本概要
- 类型:创建型
- 目标:一个类仅有一个实例
- 场景:
- 共享计数器
- 应用配置
- 优点:
- 节省内存
- 方便控制访问(全局访问一个入口)
- 缺点:
- 不便于扩展
- 实现要点:
- private 构造
- 并发安全
- 懒加载
- 序列化反序列化
- 反射
- 涉及内容:
- 反编译
- 内存可见性
- 多线程debug
懒汉实现
特点
- 延迟加载
- 非线程安全
- 可以加锁保证安全,但创建后后续读也会锁上

并发问题测试
- 问题分析:当t1进入new时切换线程 t2也可以进入new

- 多线程断点调试
- 设置测试函数主线程断点,与单例构造里判断与赋值的断点(线程断点,Suspend选Thread)


- 启动调试后观察到三个线程的RUNNING

- 切换至Thread-1单步步入getInstance()

- instance为null,正常步入new

- 切换thread-2,同样正常步入new

- 两边继续单步,t1得到2003

- t2得到2004

- LazySingleton被创建两次,如果在t1 print之前t2 完成new,那么最后t2赋值的为当前实例

- 如果先卡住t2在new,令t1完成 再继续t2完成,会出现不同实例



- 更换为synchronized方法后,调试看到t2被阻塞(MONITOR),并发问题解决

- 重复测试 - 不上锁


- 重复测试 - Synchronized锁getInstace方法


实现小结
- synchronized写方法体上会使得即使创建实例之后每次读取也会加锁
- 更高效的实现应写入方法体中,已有实例不再加锁,直接返回
- 没有实例先拿锁,如果等到拿到锁的时候还没有再创建
改进懒汉
引入
- 相比于在方法体上上锁使得后续读被阻塞,可以在方法内部上锁,每次先进行判断,非空就不再上锁
- 但new LazySingleton()创建操作与instance =的赋值操作并不是原子的,可能发生指令重排序引起并发安全问题

- 所以实现上还需要补充volatile禁止重排序(初始化与设置字段值可以在单线程中重排序,不影响执行结果)
实现与测试
- 需要两次检查,外边避开后续读上锁,里面防止除了第一个拿到锁的其他等待锁的线程重复创建



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

实现与测试



- 只要不调用getInstance,是不会触发子类构造的,所以也是懒加载


注:如果在IDEA里调试时,将鼠标放在了instance上,是会触发懒加载的,以下是同一函数的调用,结果不一样,区别只在于是否将鼠标放置在instance上



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

序列化保证单例
问题引入
- 不做处理的情况


- 定位到输入流类里readObject,进一步步入到readOrdinaryObject观察到通过反射创建了新实例

不正确的解决方法
- 创建新实例之后检查是否有readResolve方法,有就调用并用其返回值覆盖新建的对象


- 字符串写死的方法名”readResolve”



- 可以自行写readResolve方法,覆盖读取时新建的结果


反射保证单例
问题引入
- 不处理的情况


问题解决
- 类构造函数中检查,禁止重复创建(只使用饿汉|静态内部类,初始创建已经赋值instance,且只可用于防反射)

- 用于防止反射(反射调构造器newInstance)时重复

注:懒汉反射创建的是不会更新字段里的数据的,所以先反射再getInstance会出现两个实例,防不住





思考:带参构造是否能解决?
- 注:构造函数检查的方法防不住序列化,因为序列化并没有调用对应类的构造函数
- 参考以下情况,去除掉readResolve,如果调用了构造函数,那么测试应该看到IllegalStateException

- 但实际却是AssertionError,意味着readObject正常返回了,没有被构造函数异常阻止

- 重新回到源码,到断点为止,虽然读取了HungrySingleton的类描述信息,但是里面给到的构造器是Object的构造器,且这instance方法并不是反射测试里的构造器类型(Constructor),是ObjectStreamClass类上的

- 里面主要内容就是调用Object类构造器



- 可以看到Constructor里this为Object

- 调用对象ca的newInstance后直接转成了目标类型

- 然后进行序列化数据读取

- 实验下来就是反序列化创建了对应类的对象但没有调用对应类的构造函数
枚举Enum单例(推荐)
功能测试
- 并发安全 - JVM负责创建
- 序列化

- 带数据测试


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


反射
- 枚举没有空参构造,直接获取会异常


- 如果按照对应参数定义获取,会被newInstance方法中判断拦截抛异常


反编译结果
- JAD反编译
- 可以看到枚举的实现
- final class无法被继承
- 继承Enum类
- 实例为public static final,在static代码块完成初始化


- 作者:CamelliaV
- 链接:https://camelliav.netlify.app/article/singleton?target=comment
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。