网站首页 > 技术文章 正文
大家好,我是三乙己。考上大家一考:"单例模式的单例,怎样写的?"
"不就是构造方法私有化么?"
”对呀对呀!……单例模式有七种写法,你知道么?“
言归正传……
单例模式(Singleton Pattern)可以说是最简单的设计模式了。
用一个成语来形容单例模式——“天无二日,国无二主”。
什么意思呢?就是当前进程确保一个类全局只有一个实例。
那单例模式有什么好处呢?[1]
- 单例模式在内存中只有一个实例,减少了内存开支
- 单例模式只生成一个实例,所以减少了系统的性能开销
- 单例模式可以避免对资源的多重占用
- 单例模式可以在系统设置全局的访问点
那单例模式是银弹吗?它有没有什么缺点?
- 单例模式一般没有接口,扩展很困难
- 单例模式不利于测试
- 单例模式与单一职责原则有冲突
那什么情况下要用单例模式呢?
- 要求生成唯一序列号的环境
- 在整个项目中需要一个共享访问点或共享数据
- 创建一个对象需要消耗的资源过多
- 需要定义大量的静态常量和静态方法(如工具类)的环境
接下来,进入今天的主题,我们来看看单例模式的七种写法!
1、饿汉式(线程安全)
public class Singleton_1 {
private static Singleton_1 instance=new Singleton_1();
private Singleton_1() {
}
public static Singleton_1 getInstance() {
return instance;
}
}
饿汉式,就像它的名字,饥不择食,定义的时候直接初始化。
因为instance是个静态变量,所以它会在类加载的时候完成实例化,不存在线程安全的问题。
这种方式不是懒加载,不管我们的程序会不会用到,它都会在程序启动之初进行初始化。
所以我们就有了下一种方式
2、懒汉式(线程不安全)
public class Singleton_2 {
private static Singleton_2 instance;
private Singleton_2() {
}
public static Singleton_2 getInstance() {
if (instance == null) {
instance = new Singleton_2();
}
return instance;
}
}
懒汉式 是什么呢?只有用到的时候才会加载,这就实现了我们心心念的懒加载。
但是!
它又引入了新的问题?什么问题呢?线程安全问题。
图片也很清楚,多线程的情况下,可能存在这样的问题:
一个线程判断instance==null,开始初始化对象;
还没来得及初始化对象时候,另一个线程访问,判断instance==null,也创建对象。
最后的结果,就是实例化了两个Singleton对象。
这不符合我们单例的要求啊?怎么办呢?
3、懒汉式(加锁)
public class Singleton_3 {
private static Singleton_3 instance;
private Singleton_3() {
}
public synchronized static Singleton_3 getInstance() {
if (instance == null) {
instance = new Singleton_3();
}
return instance;
}
}
最直接的办法,直接上锁呗!
但是这种把锁直接方法上的办法,所有的访问都需要获取锁,导致了资源的浪费。
那怎么办呢?
4、懒汉式(双重校验锁)
public class Singleton_4 {
//volatile修饰,防止指令重排
private static volatile Singleton_4 instance;
private Singleton_4() {
}
public static Singleton_4 getInstance() {
//第一重校验,检查实例是否存在
if (instance == null) {
//同步块
synchronized (Singleton_4.class) {
//第二重校验,检查实例是否存在,如果不存在才真正创建实例
if (instance == null) {
instance = new Singleton_4();
}
}
}
return instance;
}
}
这是比较推荐的一种,双重校验锁。
它的进步在哪里呢?
我们把synchronized加在了方法的内部,一般的访问是不加锁的,只有在instance==null的时候才加锁。
同时我们来看一下一些关键问题。
- 首先我们看第一个问题,为什么要双重校验?
大家想一下,如果不双重校验。
如果两个线程一起调用getInstance方法,并且都通过了第一次的判断instance==null,那么第一个线程获取了锁,然后实例化了instance,然后释放了锁,然后第二个线程得到了线程,然后马上也实例化了instance。这就不符合我们的单例要求了。
接着我们来看第二个问题,为什么要用volatile 修饰 instance?
我们可能知道答案是防止指令重排。
那这个重排指的是哪?指的是instance = new Singleton(),我们感觉是一步操作的实例化对象,实际上对于JVM指令,是分为三步的:
- 分配内存空间
- 初始化对象
- 将对象指向刚分配的内存空间
有些编译器为为了性能优化,可能会把第二步和第三步进行重排序,顺序就成了:
- 分配内存空间
- 将对象指向刚分配的内存空间
- 初始化对象
所以呢,如果不使用volatile防止指令重排可能会发生什么情况呢?
在这种情况下,T7时刻线程B对instance的访问,访问的是一个初始化未完成的对象。
所以需要在instance前加入关键字volatile。
- 使用了volatile关键字后,可以保证有序性,指令重排序被禁止;
- volatile还可以保证可见性,Java内存模型会确保所有线程看到的变量值是一致的。
5、单例模式(静态内部类)
public class Singleton_5 {
private Singleton_5() {
}
private static class InnerSingleton {
private static final Singleton_5 instance = new Singleton_5();
}
public static Singleton_5 getInstance() {
return InnerSingleton.instance;
}
}
静态内部类是更进一步的写法,不仅能实现懒加载、线程安全,而且JVM还保持了指令优化的能力。
Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会加载静态内部类InnerSingleton类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,同时类加载的过程又是线程互斥的,JVM帮助我们保证了线程安全。
6、单例模式(CAS)
public class Singleton_6 {
private static final AtomicReference<Singleton_6> INSTANCE = new AtomicReference<Singleton_6>();
private Singleton_6() {
}
public static final Singleton_6 getInstance() {
//等待
while (true) {
Singleton_6 instance = INSTANCE.get();
if (null == instance) {
INSTANCE.compareAndSet(null, new Singleton_6());
}
return INSTANCE.get();
}
}
}
这种CAS式的单例模式算是懒汉式直接加锁的一个变种,sychronized是一种悲观锁,而CAS是乐观锁,相比较,更轻量级。
当然,这种写法也比较罕见,CAS存在忙等的问题,可能会造成CPU资源的浪费。
7、单例模式(枚举)
public enum Singleton_7 {
//定义一个枚举,代表了Singleton的一个实例
INSTANCE;
public void anyMethod(){
System.out.println("do any thing");
}
}
调用方式:
@Test
void anyMethod() {
Singleton_7.INSTANCE.anyMethod();
}
《Effective Java》作者推荐的一种方式,非常简练。
但是这种写法解决了最主要的问题:线程安全、自由串行化、单一实例。
总结
从使用的角度来讲,如果不需要懒加载的话,直接饿汉式就行了;如果需要懒加载,可以考虑静态内部类,或者尝试一下枚举的方式。
从面试的角度,懒汉式、饿汉式、双重校验锁饿汉式,这三种是重点。双重校验锁方式一定要知道指令重排是在哪,会导致什么问题
猜你喜欢
- 2025-05-08 java2(JaVa2实用教程第6版Pdf)
- 2025-05-08 Java 类初始化顺序解析与高并发优化
- 2025-05-08 Java基本程序设计结构(上)(java程序设计的三种结构是什么)
- 2025-05-08 Java 阴历阳历转换(java转化日期格式)
- 2025-05-08 Kafka消息可靠传输之幂等、事务机制
- 2025-05-08 Java JVM原理与性能调优:从基础到高级应用
- 2025-05-08 Spring中使用的设计模式,你都能说全吗?
- 2025-05-08 一篇文章彻底搞懂JVM加载中初始化的时机
- 2025-05-08 java反射之获取属性Field总结(java反射获取类名)
- 2025-05-08 字符型常量和字符串常量的区别(字符型常量和字符串常量的区别是什么?)
- 1517℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 594℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 521℃MySQL service启动脚本浅析(r12笔记第59天)
- 489℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 489℃启用MySQL查询缓存(mysql8.0查询缓存)
- 477℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 456℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 454℃MySQL server PID file could not be found!失败
- 最近发表
-
- PS所有滤镜的说明(六)(ps滤镜详解)
- 5款小白也能用的在线图片编辑器!电商效率飙升就靠它!
- Java变量(java变量有什么作用)
- Java面试常见问题:Java注解(java中的面试题)
- Java编程入门第一课:HelloWorld(java编程从入门到实践)
- Java基础教程:Java继承概述(java里继承的概述)
- java基础之——访问修饰符(private/default/protected/public)
- 如何规划一个合理的JAVA项目工程结构
- 将机器指令翻译成 JavaScript -- 终极目标
- Web 服务器基准测试:Go vs. Node.js vs. Nim vs. Bun
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)