输出:
可以看到,他们并不是同一个对象,这意味着饿汉式单例模式被破坏了
事实上,使用反射后,无论是饿汉式、懒汉式、升级的双重校验锁机制、静态内部类机制,都是不安全的
2.2 懒汉式
在懒汉式的实现中,默认不会进行实例化,什么时候用到了,什么时候 New,从而节约资源
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
System.out.println(Thread.currentThread().getName());
}
public static LazySingleton getInstance() {
if (lazySingleton == null) lazySingleton = new LazySingleton();
return lazySingleton;
}
}
复制代码
但是这个实现在多线程的环境下是不安全的,试想以下,当 lazySingleton
为空时,试想一下,当lazySingleton
为空时,有多个线程同时通过了if (lazySingleton == null)
的判断,这样就会导致new 被执行了多次,使用代码复现一下:
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> LazySingleton.getInstance()).start();
}
}
复制代码
控制台输出:
可以看到,实例化代码被执行了三次,为了解决线程安全的问题有两个方法:
-
在
getInstance()
方法的层级上加关键字synchronized
-
引入双重检测锁
3.3 双重校验锁
为了解决懒汉式线程不安全的问题,可以引入双重校验锁的机制,双重检验锁也是一种延迟加载,并且较好的解决了在确保线程安全的时候效率低下的问题
以下是代码实现:
public class DCLSingleton {
private volatile static DCLSingleton dclSingleton;
private DCLSingleton() { }
public static DCLSingleton getInstance() {
if (dclSingleton == null) {
synchronized (DCLSingleton.class) {
if (dclSingleton == null) dclSingleton = new DCLSingleton();
}
}
return dclSingleton;
}
}
复制代码
在这个实现中,对比一下懒汉式在方法上加锁,那么每次调用那个方法都要获得锁,释放锁,等待等待……而双重校验锁锁住了部分的代码。进入方法如果检查为空才进入同步代码块,这样很明显效率高了很多
3.3.1为什么要双重校验
那在这里为什么 dclSingleton == null
要判断两次,假设我们先去掉第二次的判断。
如果两个线程一起调用 getInstance()
方法,并且都通过了第一次的判断 dclSingleton == null
,那么第一个线程获取了锁,然后进行了实例化后释放了锁,然后第二个线程会开始执行,然后马上也进行了实例化,这就尴尬了。
所以加上第二次判断后,先进来的线程判断了一下,哦?为空,我创建一个,然后创建一个实例之后释放了锁,第二个线程进来之后,哎?已经有了,那我就不用创建了,然后释放了锁,开开心心的完成了单例模式。
3.3.2 为什么要使用关键字volatile
对于 new 操作来说,它不是一个原子性操作,他在底层大概发生了以下三件事:
-
在堆中分配内存空间
-
执行它的构造方法,初始化对象
-
在栈中定义引用,再把这个对象指给堆中的实际对象
我们期望它是按顺序发生的,但是由于Java的指令重排机制,可能在没有初始化对象时,就把栈中定义的引用指给堆中的空间,当第二个线程再进来的时候,第一次判定是否为空,他认为不为空,于是将还没有进行初始化的对象返回了;这就是为什么要加上关键字volatile的原因。
3.4 静态内部类实现
当 InnerClassSingleton
类加载时,静态内部类 InnerClass
没有被加载进内存。只有当调用 getInstance()
方法从而触发 InnerClass.INSTANCE
时 InnerClass
才会被加载,初始化实例 INSTANCE。
这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。
public class InnerClassSingleton {
private InnerClassSingleton() { }
public static InnerClassSingleton getInstance() {
return InnerClass.INSTANCE;
}
static class InnerClass {
private static final InnerClassSingleton
INSTANCE = new InnerClassSingleton();
}
}
复制代码
3.5 枚举
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次
外部调用直接使用 Singleton.INSTANCE
,简单粗暴。
由于 Enum 实现了 Serializable 接口,所以不用考虑序列化的问题(其实序列化反序列化也能导致单例失败的,但是我们这里不过多研究),并且加载的时候 JVM 能确保只加载一个实例,所以它是线程安全的,而且反射无法破解这种单例模式的实现
public enum Singleton {
INSTANCE;
}
复制代码
总结
–
本文论述了单例模式常见的五种实现方式,在《Effect Java》中,作者极力推崇使用枚举类来实现单例模式,并认为这个实现是单例模式的最佳实践
感谢阅读,希望本文对你有所帮助
作者:Java雏鸡开发
链接:https://juejin.cn/post/6995836114032394277
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。
本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。
[外链图片转存中…(img-2GRHths9-1713518991975)]
[外链图片转存中…(img-frdbY38h-1713518991976)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!