coldsmog开发笔记
JS 事件笔记
Ckeditor 上传WPS图片失败问题
Springboot
SpringBoot 统一异常处理
Springboot 引入外部jar包
Springboot 打成war包
Springboot 多环境配置
SpringBoot @Scope注解学习
Springboot 快速生成项目API文档
SpringCache 缓存
Spring jetcache 二级缓存
Springboot 按条件装配类
FastJson的JsonPath语法
正则表达式语法
Spring 路径匹配
Feign 基础数据操作
监控Feign调用metrics
Springboot feign的性能优化
Jackson 设置序列化属性
SpringBoot 集成 Spring Data Mongodb 操作 MongoDB
MongoDB 的一些注意事项
MongoDB 指令对比
Jackson 解析XML
Springboot Redis注册
SpringBoot RedisTemplate批量插入
Springboot 指标监控Micrometer
springboot validation 注解校验
springboot 引入配置
Springboot 静态文件处理
Springboot 导出csv文件
Springboot 事件驱动(发布/订阅模式)
Springboot 启动过程和扩展点
Springboot 优化停机上下线
Spring自动装配 - 干饭角度学习
Springboot ShardingJDBC
Springboot的重试
springboot 动态修改端口
Oracle
Oracle 中实现自增ID
Oracle 定时任务
Oracle 解锁临时表
Oracle 检查连接数
Oracle 表空间
Oracle 解释执行SQL
markdown作图(适用typora)
服务器压测
业务对象层和数据层
并发限流处理
中间件
Yarn的使用
Dubbo学习笔记-RPC扩展和本地Mock
Dubbo学习笔记-泛化实现进行mock
Redis缓存穿透,缓存击穿,缓存雪崩
Galera 集群说明
Pip 镜像
pip 使用
MySQL命令行
数据库缓存双写方案
Git相关操作
Redis 操作时间复杂度一览
nacos 杂记
mybatis 散记
shardingjdbc
一次线上事故排查发现的Caffeine缓存死锁问题
设计模式
重新讲讲单例模式和几种实现
更优雅地实现策略模式
Http-headers
Prometheus 杂散笔记
JAVA 散记
CompletableFuture
Gson、FastJson、Jackson、json-lib对比总结
jackson 时间的序列化踩坑
JVM
自定义注解
mysql类型和java类型 转换一览表
枚举维护一个Map<value, Enum>的映射
Java中String +、concat、StringBuilder、StringBuffer 性能对比
TraceId 使用
MySQL 多数据源处理
Mybatis-plus 流式查询
JAVA发送win 桌面通知
idea 启动项目失败非代码问题杂记
Lambda 简述
Arthas 使用笔记
一种链式更新数据的数据模式
Skywalking 新增中间件插件
Redission 使用
数据导出为图片
IDEA 的热重启
Netty 工具类
maven 插件
本文档使用 MrDoc 发布
-
+
首页
重新讲讲单例模式和几种实现
一、什么讲单例模式 单例模式,最简单的理解是对象实例只有孤单的一份,不会重复创建实例。 这个模式已经很经典了,经典得我不再赘述理论,只给简单注释,毕竟教科书详尽太多。 解决 [sonar RSPEC-2168](https://jira.sonarsource.com/browse/RSPEC-2168 ) 异味的时候,发现目前业界推荐的单例模式和教科书上的已经有了较大差异,双重锁定不再推荐,甚至业内认为的最优方案不在sonar的推荐里 于是提笔记录,顺带补充了自己对多线程单例的理解 。 二、经典的单线程单例 这个部分没有改动,简单而经典,大致源码如下 ``` public final class SignUtil { /** * 需要保持单例的对象 */ private static Object object; /** * 只允许SignUtil.getInstance获取对象,也就是入口唯一 */ private SignUtil() { } /** * 对象的唯一出口 调用时才初始化(懒加载) * @return Object 确保单线程情况下这里出去就是初始化好的 */ public static Object getInstance() { if (null == object) { object = new Object(); } return object; } /** * 内部函数也必须使用 getInstance这个入口 */ public static String getString() { return getInstance().toString(); } } ``` 三、经典的双重锁定多线程单例 (JDK5-JDK7继续适用) ``` public final class SignUtil { /** * 需要保持单例的对象 * 这里需要声明对象是易失的,因为object = new Object()不是一个原子操作,是被分拆为了实例化和初始化,一个申请空间,一个分配值 * 那么就有可能出现 C在第三瞬间进入getInstance函数,发现null!=object,此时对象实例化了但没初始化就直接返回,是个高危操作 */ private volatile static Object object; /** * 只允许SignUtil.getInstance获取对象,也就是入口唯一 */ private SignUtil() { } /** * 对象的唯一出口 * * @return Object 多线程情况下这里出去就是初始化好的 */ public static Object getInstance() { // 第0瞬间 A B 两个线程同时初始化,一看都是null嘛 if (null == object) { // 第1瞬间 A B都进来了,因为不能重复初始化,所以被synchronized锁约束开始竞争. // A 赢了SignUtil的对象锁,B 只能等着 synchronized (SignUtil.class) { // 这里为什么不直接object = new Object()呢? // 因为B还等着呢,直接初始化就拦不住B再来一次初始化了. if (null == object) { // 第2瞬间, A终于初始化成功,且B不会重新初始化了. object = new Object(); // 第3瞬间,因为object被volatile约束了,可以视为原子操作,补上最后一个漏洞,成功返回。 } } } return object; } /** * 内部函数也必须使用 getInstance这个入口 */ public static String getString() { return getInstance().toString(); } } ``` 四、 JDK8 以后的多线程单例 可以看到,三的要点太多了,很经典的双重锁定,但是不够简单优雅。目前更推荐下面两种格式 4.1 JDK8 带来的一个特性之一即是synchronized关键字,从原来的monitor重量级锁,转变成了由偏向锁进行逐级升级到重量级锁。换句话说,使用synchronized的代价被降低了,我们可以将上面的函数进行一个改进,让它保持简单和优雅。 但是代价依旧存在,以下适合并发冲突不严重的项目。 ``` public final class SignUtil { /** * 需要保持单例的对象 */ private static Object object; /** * 只允许SignUtil.getInstance获取对象,也就是入口唯一 */ private SignUtil() { } /** * 对象的唯一出口 是的,仅比单线程版多了一个synchronized * @return Object 由于synchronized,同一瞬间只能有一个对象进行获取实例 */ public static synchronized Object getInstance() { if (null == object) { object = new Object(); } return object; } /** * 内部函数也必须使用 getInstance这个入口 */ public static String getString() { return getInstance().toString(); } } ``` 4.2 利用静态内部类的初始化特性 很巧妙地利用了jvm的类加载机制。那就是静态内部类的延迟加载性完成单例。 ``` public final class SignUtil { /** * 利用jvm的初始化规则 静态内部类的静态内部对象,只有在调用时才对静态类开始初始化, * 类的初始化过程是线程安全的,所以也只有一个线程能进行初始化 */ private static class Node { /** * 在读写调用时才真正初始化,也就是懒加载 */ private static final Object object = new Object(); } /** * 只允许SignUtil.getInstance获取对象,也就是入口唯一 */ private SignUtil() { } /** * 不再是对象的唯一出口,其他地方也只要读写都能完成初始化 * * @return Object 调用时,会触发内部静态类的初始化,返回时,初始化已完成 */ public static Object getInstance() { return Node.object; } /** * 内部函数终于不用再依赖 getInstance这个入口 */ public static String getString() { return Node.object.toString(); } } ``` 五、 有没有办法让单例模式不单例? 听起来很魔鬼,但实际上,上述的多线程程单例都有两个共同的缺陷可以做到:a 反射Constructor::setAccessible将私有构造函数改为公有函数 b.序列化时还是会返回多个实例。 解决方法为改造构造函数和申明readResolve函数,参考如下,解决方案是通用的。 ``` public final class SignUtil { private static volatile boolean init = false; private static class Node { private static final Object object = new Object(); } /** * 添加一个volatile的变量去判断,防止反射初始化 * 第二次初始化会抛出类强制转换异常 当然你也可以用其他运行时异常 */ private SignUtil() { if (!init) { init = true; } else { throw new ClassCastException(); } } public static Object getInstance() { return Node.object; } public static String getString() { return Node.object.toString(); } /** * 反序列化时直接返回单例的对象,这么写的原因在 ObjectInputStream::readUnshared里 */ private Object readResolve() { return Node.object; } } ``` 六、枚举单例 6.1 单元素枚举单例 和4.2一样,《Effective Java 》找到了另一种利用jvm类加载机制实现单例的方法:单元素枚举单例。 这里有几个前提: - Enum禁用了默认序列化。Enum::readObject、Enum::readObjectNoData约束了枚举对象的默认反序列化,保证序列化安全 - Enum提供了自己的序列化。Enum::toString 返回的是属性名称name,再通过Enum::valueOf把name转回实例,保证了枚举不会被“退货”(这个直译了,大概是final且不会被clone的意思)。 - 这里说一下valueOf的底层是Class::enumConstantDirectory,作用是调用时,生产一个Map<name, 枚举>的映射,而这个map很像单线程单例模式,但他不是静态共享变量,所以是线程安全的, 不得不说,单元素枚举的确成功避免了重重的繁琐,但代价是没有了懒加载的特性,变成了饿汉模式 ``` public enum SignUtil { /** * 从javap的反编译结果看,会变成一个类公开的静态变量,也就是饿汉模式 * public static final SignUtil INSTANCE = new SignUtil(); * 也就是会在加载类时直接初始化INSTANCE对象,而object对象是在构造时作为内部变量初始化,而构造函数是由jvm保证的 */ INSTANCE; /** * 由于INSTANCE单例,所以object才是单例的 */ private final Object object = new Object(); public Object getInstance() { return object; } public String getString() { return object.toString(); } } ``` 补一下javap反编译后的结果 ``` public final class SignUtil extends java.lang.Enum<SignUtil> { public static final SignUtil INSTANCE; private final java.lang.Object object; private static final SignUtil[] $VALUES; public static SignUtil[] values(); public static SignUtil valueOf(java.lang.String); private SignUtil(java.lang.Object); public java.lang.Object getInstance(); public java.lang.String getString(); static {}; } ``` 6.2 多元素枚举的单例呢? 由于多元素枚举的构造函数可以被反射修改成公用函数并设置object,但由于INSTANCE和object都是final约束的,所以修改就会报错,以此保证了单例性。 所以按照理解 多元素枚举也能完成单例,只是适用场景偏少 ``` public enum SignUtil { /* * 对的,唯一的区别就是由无参变成了有参构造,本质是不变的饿汉 * public static final SignUtil INSTANCE = new SignUtil(new Object()); */ INSTANCE(new Object()), OTHER(new Object()); private final Object object; private SignUtil(Object object) { this.object = object; } public Object getInstance() { return this.object; } public String getString() { return this.object.toString(); } } ```
寒烟濡雨
2022年3月25日 18:06
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码