单例模式
概念
熟悉面向对象编程都知道“类”和“对象”的关系,所谓的“单例”描述的是对象,即有且仅有一个对象。一个类有且仅有一个对象,那么这个类就叫做单例类。
为什么需要使用单例?
这个问题其实很无厘头,就像“计算机”为什么叫做“计算机”。
回到单例的概念,有且仅有一个对象。这样做有什么好处?
节约内存、节省对象创建开销等。
本质上来说呢,如果我们的业务不需要构建多个对象,那么就可以使用单例模式,比如一些工具类的使用。
五种实现单例的操作
注意事项
无论是哪种方式实现的单例,都要注意以下几点:
- 构造器私有化,防止外界去new
- 对外暴露一个公共的获取单例实例的方法
- 是否支持懒加载(延迟加载)
- 是否线程安全
1、饿汉式
public class AnyClass {
private static final AnyClass INSTANCE = new AnyClass();
private AnyClass(){}
public static AnyClass getInstance(){
return INSTANCE;
}
}
简单易懂,不宜出错。
类加载的时候,这个静态实例就已经创建好了,默认是线程安全的。(当然,这里面涉及到的原因就有些复杂了,需要深入理解类加载机制)
2、懒汉式
public class AnyClass {
private static AnyClass INSTANCE;
private AnyClass(){}
public static synchronized AnyClass getInstance(){
if(INSTANCE == null){
INSTANCE = new AnyClass();
}
return INSTANCE;
}
}
这个方式就是比较懒嘛,需要用到的时候再创建实例。
但是大家也看到了,getInstance方法加上了synchronized,拿不到锁的线程只能排队等候,高并发的场景下还会有“等待拿锁线程饿死”的现象。
3、双检索优化
public class AnyClass {
private static volatile AnyClass INSTANCE;
private AnyClass(){}
public static AnyClass getInstance(){
if(INSTANCE == null){
synchronized (AnyClass.class){
if(INSTANCE == null){
INSTANCE = new AnyClass();
}
}
}
return INSTANCE;
}
}
这里的重头戏在于volatile
和双重if判断
:
相较于前面的“懒汉式”,优化了什么?
减少了锁操作,只有在单例未创建的时候,才去抢锁执行对应的创建逻辑。
volatile的作用?
- 保障内存可见性,保证一个线程操作INSTANCE时,另一个线程“知道”
- 防止指令重排,让对象完整的创建出来
4、静态内部类
public class AnyClass {
private AnyClass(){}
private static class InstanceHolder {
private static final AnyClass INSTANCE = new AnyClass();
}
public AnyClass getInstance(){
return InstanceHolder.INSTANCE;
}
}
也很简单,本质上并没有内部类这个概念,编译过后都是外层顶级类,只有调用getInstance方法时,才去进行类加载等一系列操作,JVM该过程的线程安全的。
既实现了线程安全,又实现了懒加载。
5、枚举
public enum AnyEnum {
INSTANCE;
private final String name;
AnyEnum(){
this.name = "kaiven";
}
public String getName(){
return this.name;
}
}
这个没有什么好说的,利用了枚举类天然的单例特性,不过感觉还是怪怪的,这样写。
破坏单例的操作
原则意义上,上面那些操作已经能够保证对象的单例化了。但是奈何需要去面试呀:
1、反射入侵
public class AnyClass {
private static final AnyClass INSTANCE = new AnyClass();
private AnyClass(){}
public static AnyClass getInstance(){
return INSTANCE;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<AnyClass> constructor = AnyClass.class.getDeclaredConstructor();
constructor.setAccessible(true);
System.out.println(constructor.newInstance() == constructor.newInstance()); // false
}
}
通过反射调用私有构造器,你能怎么办?
还能怎么办?抛异常呗,不允许通过反射进行对象的实例化:
private AnyClass(){
throw new RuntimeException("你家屋头,不要乱搞");
}
笔者觉得这是一个无聊的事情。。。
2、序列化与反序列化安全
这里就不演示了,很显然,大家都懂的,一定不是同一个对象的。
这里讲一下解决方案:
public class AnyClass implements Serializable {
private static final AnyClass INSTANCE = new AnyClass();
private AnyClass(){
throw new RuntimeException("你家屋头,不要乱搞");
}
public static AnyClass getInstance(){
return INSTANCE;
}
@Serial
public Object readResolve(){
return INSTANCE;
}
}
关于readResolve,这里贴一篇文章:https://blog.csdn.net/zhouxiaozheng213/article/details/137244943
单例的问题
这些问题都不叫做问题,当你决定要用的时候,那就不存在这些问题:
- 单例类无法继承
- 横向扩展困难(这个笔者角色不是问题,我用它是因为我的业务只需要一个实例就行了)
不用作用范围的单例模式
1、线程级别的单例
ThreadLocal或者ConcurrentHashMap,ThreadLocal的本质也是后者。
2、容器范围内的单例
最好的例子就是Spring容器
,我们只需要写好注解,由对应的容器来帮我们管理这些单例对象。
总结
关于单例模式,重点关注五种实现的方式以及何时使用单例模式,其他的了解一下就行了。
设计这种东西,没有对错,只有好坏,需要根据不同的业务场景进行定夺,不要不设计,也不要过度设计。
2024/12/30
writeBy kaiven