文章目录
- 前言
- 1. 什么是单例模式
- 2. 饿汉模式
- 3. 懒汉模式 --- 单线程版
- 4. 懒汉模式 --- 多线程版
- 5. 懒汉模式 --- 多线程改进版
- 总结
前言
本文主要给大家讲解多线程的一个重要案例 — 单例模式.
关注收藏, 开始学习吧🧐
1. 什么是单例模式
单例模式是一种很经典的设计模式, 那么什么叫做设计模式呢?
设计模式好比象棋中的 “棋谱”.
红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式有很多种写法, 在这里我们主要讲解 “饿汉” 和 “懒汉” 两种.
2. 饿汉模式
核心思想: 类加载的同时, 创建实例.
class Singleton {
private static Singleton instanse = new Singleton();
public static Singleton getInstance() {
return instanse;
}
private Singleton() {};
}
注意:
private static Singleton instanse = new Singleton();
被 static 修饰, 该属性是类的属性, JVM 中, 每个类的类对象只有唯一一份, 类对象里的这个成员自然也是唯一一份了.private Singleton() {};
将构造方法设为 private, 就可以将外部的 new 操作给禁用掉.- 此时, 在类内部把实例创建好, 同时禁止外部重新创造实例, 就可以保证单例的特性了.
由于构造方法设为 private, 导致 new 操作被禁用, 我们只能通过类方法 .getInstanse
来创建实例, 可以看到, 这样先后创建的 s1 和 s2 实例, 其实是同一个实例.
3. 懒汉模式 — 单线程版
核心思想: 类加载的时候不创建实例. 第一次使用的时候才创建实例.
class SingletonLazy {
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {};
}
注意点与饿汉模式差不多, 但是区别在于, 懒汉模式是 “非必要不创建”, 可以看到, instance 实例对象是在调用类方法时才创建的.
4. 懒汉模式 — 多线程版
现在问题来了, 上述两个模式, 是否能构保证线程安全呢?
多个线程下调用 getInstance 方法, 是否会出现问题呢?
回想一下我们之前讲解的线程不安全的几个原因. 可以推断饿汉模式下, 线程是安全的, 因为他只是读数据, 并没有进行写数据.
但是多线程下, 懒汉模式可能无法保证创建对象的唯一性, 线程不安全.
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)
我们可以通过加锁, 利用 synchronized 关键字就可以改善这里的线程安全问题.
class SingletonLazy {
private static SingletonLazy instance = null;
public synchronized static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy() {};
}
5. 懒汉模式 — 多线程改进版
加锁其实是一个比较低效的操作, 因为他会造成阻塞等待, 非必要还是不要进行加锁.
以下代码在加锁的基础上, 做出了进一步改动:
- 使用双重 if 判定, 降低锁竞争的频率.
- 给 instance 加上了 volatile.
class SingletonLazy {
private static volatile SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {};
}
理解双重 if 判定 / volatile:
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.
- 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
- 同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile .
- 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作.
- 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
- 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.
总结
✨ 本文讲解了线程安全下的单例模式, 由于饿汉模式只是读操作, 天生就是安全的, 而懒汉模式不是安全的, 因为有写操作, 我们通过加锁, 并利用双重 if 来减少不必要的加锁操作, 再使用 volatile 禁止指令重排序, 使其变得安全.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.
再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!