本期讲解单例模式的饿汉模式与懒汉模式,以及如何解决懒汉模式造成线程的不安全问题。
目录
什么是单例模式?
1. 饿汉模式
2. 懒汉模式
2.1 懒汉模式单线程版
2.2 懒汉模式多线程版
3. 解决懒汉模式不安全问题
3.1 保证原子性
3.2 防止指令重排序
什么是单例模式?
首先,单例模式是一种设计模式。何为设计模式,设计模式类似于固定的套路。例如考驾照的科目二项目,教练会总结出一些点位,因此我们按照这些点位去练习然后考试就能很顺畅的通过。在 Java 中也是如此,常见的就是开发中前辈设计好的一些案例,我们直接拿来用即可。
单例模式是在进程中有且仅有一份实例的模式,所以我们称之为单例。此外单例模式分为饿汉模式与懒汉模式。
通过上图,我们可以看到。thread1 - thread3 都共用 Singleton 这个实例,这样的一个模式就是单例模式。
1. 饿汉模式
看到饿汉二字,我们就会想到这是一种饥渴的状态,有一种一看到饭就冲上去吃的感觉。因此,饿汉模式它是一种类加载时就创建对象的一种模式,如下代码:
//自定义类singleton
class Singleton {
//创建一个对象
private static Singleton instance = new Singleton();
//提供一个获取instance的方法
private static Singleton getInstance(){
return instance;
}
}
当以上代码中的自定义类 singleton 被加载后,就会创建一个 instance的对象。这时候我们就可以通过一个获取 instance 对象的方法 getInstance 来使用这个实例。由于 singleton 类中的所有成员变量与成员方法都是被 private 修饰,因此达到了封装效果也体现出了单例模式的唯一性。
饿汉模式强调一个饥渴,类一被加载就创建了一个对象。它不存在线程安全问题,当多个线程调用这个饿汉模式时得到的都是同一个实例,并不重新创建实例。
以上的饿汉模式,设计得还是有问题的。如果我们新建了一个实例,这样就不能保证饿汉模式是一个单例模式,如以下代码:
public static void main(String[] args) {
//s1和s2都是同一个实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//s3新建了一个实例
Singleton s3 = new Singleton();
}
因此,我们必须保证在 Singleton 类不能被实例化,这时我们可以在 Singleton类 中提供一个被 private 修饰的构造方法,这样无论如何 Singleton 类都不能被 new 了。如下行代码:
private Singleton(){};
2. 懒汉模式
饿汉模式体现了一种饥渴,懒汉模式给人感觉就是一种懒散的状态,一碗饭在面前爱吃不吃的感觉。因此,懒汉模式在创建实例时并不在类加载时创建对象,而是什么时候需要创建对象了就去创建,不需要则不创建。
举个例子,在家里面,吃午餐用了五个盘子,由于很懒没有及时的去洗。到了晚上,炒菜发现没盘子可用了才洗个盘子用来盛菜。剩余的四个盘子还是不洗,至于臭了还是烂了并不在意。这就是体现出懒汉模式中的“懒”状态。
2.1 懒汉模式单线程版
通过上方例子的讲解,我们可以了解到。懒汉模式在使用某个对象时,得判断该是否实例化。如果实例化过了就不创建直接返回该实例,没有则创建后返回该实例。如下流程图:
案例:通过懒汉模式创建一个自定义类 SingletonLazy ,并在 main 方法中创建两个 SingletonLazy 类的引用 s1、s2,使得 s1 等于 s2。因此,我们可以写出以下代码:
class SingletonLazy {
//创建一个SingletonLazy的实例为空
private static SingletonLazy instance = null;
//获取该实例的方法
public static SingletonLazy getInstance(){
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
//提供一个private修饰的构造方法,保证唯一性
private SingletonLazy(){};
}
public class ThreadDemo2 {
public static void main(String[] args) {
//s1和s2是同一个实例
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
运行后打印:
以上代码,与饿汉模式相比来说更符合与现实开发。当然,上述代码是单线程版的饿汉模式。因此是比较安全的,但是把以上代码应用到多线程情况下就会造成线程不安全问题。
2.2 懒汉模式多线程版
首先,我们来看下上文中创建的懒汉模式的 SingletonLazy 类的代码。
class SingletonLazy {
//创建一个SingletonLazy的实例为空
private static SingletonLazy instance = null;
//获取该实例的方法
public static SingletonLazy getInstance(){
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
//提供一个private修饰的构造方法,保证唯一性
private SingletonLazy(){};
}
在多线程的学习中,我们以及知道了造成线程不安全有线程抢占资源这个概念,其原因就是多个线程在执行过程中进行读和写操作也就是修改操作,导致的不安全问题。
多线程下,懒汉模式会导致创建多个实例,因此不能保证实例的唯一性。假如有多个线程进行调用了 getInstance 方法。线程1在执行同时,由于运行速度过快,线程2也开始执行了,导致最后创建了两个实例。这样就不叫单例模式了。
3. 解决懒汉模式不安全问题
3.1 保证原子性
在上方创建的懒汉模式中的 if 语句 和 new 操作,是不具备原子性的。其原因为在多个线程调用 getIstance 这个方法。
上文中设计的懒汉模式预期的效果为:当多个线程调用 getInstance 方法后。第一个调用 getInstance 的线程会进行 new 操作创建一个 instance 实例,其他线程调用 getInstance 方法后发现 instance 不为 null 则不进行 new 操作。
但由于线程的抢占式执行,导致第一个调用 getIstance 的线程执行到第一步后,其他线程抢占执行了并调用了 getIstance 方法,这个时候两个线程 if 语句都判断 instance 等于 null 。这时候就创建了两个 instance 对象。
这样就导致 if 语句 和 new 操作就不具备原子性(不能完整的执行)。因此,我们可以使用 synchronized 关键字来加锁,使得这两个操作具备原子性。如下代码所示:
//获取该实例的方法
public static SingletonLazy getInstance(){
//给if语句和new操作加锁
synchronized (Singleton.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
当然,以上代码加上了锁虽然保证了 if 语句和 new 操作具备了原子性,但还不算是最优的写法。我们可以想象一下,每个线程调用 getInstance 这个方法时候,都会进行锁的竞争这样就会阻塞等待,这样的时间效率是非常低的。
因此,我们可以使用双重 if 语句来减少阻塞等待。如下代码:
public static SingletonLazy getInstance(){
if (instance == null) {
//给if语句和new操作加锁
synchronized (Singleton.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
以上的代码中两条 if 语句里面的条件是一样的,但其初心不同。第一条 if 是为了判断是否有 instance 这个实例不让其他线程进入锁的竞争,第二条 if 语句是在锁下进行判断的创建唯一一个实例。
当然,可能多个线程抢占并进入到了第一条 if 语句,但第一个进入锁的线程完成了创建实例任务后,其他线程进入锁后 if 判断的实例不为空也就不会再多创建实例了。
这样的设计才是懒汉模式的标准写法,保证了实例的唯一性。但有一极端的情况,指令被重排序了,具体请看下方讲解。
3.2 防止指令重排序
有一种极端的情况,两个线程同时调用了 getInstance 方法,都进入了第一条 if 语句里面。线程1进入了锁(synchronized)的范围,但由于指令重排序导致 new 这个操作与原本执行顺序不一致。这时候,线程2进入了锁的范围,发现 instance 实例已被创建,则返回 instance 实例。
这样就会导致线程2调用的构造方法是虚无的、不知道是哪里的,造成了线程的不安全。因此,我们在初始化 instance 实例时加上 volatile 关键字,使得指令能够按照顺序进行。
volatile private static SingletonLazy instance = null;
综合起来,创建一个懒汉模式的代码如下所示:
class SingletonLazy {
//创建一个SingletonLazy的实例为空,volatile修饰保证指令顺序执行
volatile private static SingletonLazy instance = null;
//获取该实例的方法
public static SingletonLazy getInstance(){
//判断instance实例是否存在,存在则返回
if (instance == null) {
//给if语句和new操作加锁,防止多new操作
synchronized (Singleton.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
//提供一个private修饰的构造方法,保证唯一性
private SingletonLazy(){};
}
总结:
- 单例模式是在进程中有且仅有一份实例的模式。
- 单例模式分为饿汉模式与懒汉模式。
- 饿汉模式天然不存在线程不安全问题。
- 懒汉模式存在线程不安全问题,因此需要进行加锁(synchronized)操作与防止指令重排序(volatile)操作。
🧑💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。
🗃️文章收录于:Java多线程编程
🗂️JavaSE的学习:JavaSE
🗂️Java数据结构:数据结构与算法
本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~