单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例模式同时解决了两个问题, 所以违反了单一职责原则:
- 保证一个类只有一个实例
- 为该实例提供一个全局访问节点
解决方法
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。 - 新建一个静态构建方法作为构造函数。 该函数会 "偷偷" 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。 在下面的代码中大家可以看到是否符合上述的描述。
接下来下面将演示几种单例模式:
- 懒汉模式(通俗解释:比较懒,等到调用的时候,在进行实例化对象)
- 饿汉模式(通俗解释: 太饿了,想立马拥有,就在类加载的时候就完成了初始化)
懒汉模式
- 这个类是懒汉模式,并且是线程不安全的
java
public class Singleton {
// 私有构造方法,防止外部类通过new Singleton()创建对象
private Singleton(){}
// 静态变量,懒加载,延迟初始化
private static Singleton instance;
// 静态方法,创建实例
public static Singleton getInstance(){
//多个线程同时调用,可能会创建多个对象
if (instance == null){ // ①
instance = new Singleton();
}
return instance;
}
}
这时就有人就会问了,为什么是线程不安全呢?我看着很安全,嘎嘎安全啊。
那就让俺来解释解释吧!!!
当在多线程场景下,可能会有多个线程同时到达上述代码标记的①处,此时并没有实例化,所以就都符合条件,也就进行了创建多个对象了。下面有一段代码来演示。
java
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 100; i++) { //如果没有测试出来,小伙伴们可以调大数量(100 -> 1000)
new Thread(() -> {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
if (instance1 != instance2) {
System.out.println("线程不安全!创建了多个实例");
}
}).start();
}
}
}
- 懒汉模式,线程安全,但是效率不高(不推荐)
java
public class Singleton {
// 私有构造方法,防止外部类通过new Singleton()创建对象
private Singleton(){}
// 静态变量,懒加载,延迟初始化
private static Singleton instance;
// 静态方法,创建实例 添加了synchronized 升级为类锁
public static synchronized Singleton getInstance(){
//多个线程同时调用,可能会创建多个对象
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
- 双重锁同步的懒汉模式(上述方式优化版)
java
public class Singleton {
// 使用volatile修饰instance,保证多线程下的可见性,并且禁止指令重排序
private static volatile Singleton instance;
private Singleton() {
// 私有构造方法,防止外部类通过new Singleton()创建对象
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查实例是否存在,如果不存在才进入同步块 ①
synchronized (Singleton.class) {
if (instance == null) {
// 第二次检查实例是否存在,如果不存在才创建实例
instance = new Singleton(); // ②
}
}
}
return instance;
}
}
注意: 在这个实现中,volatile
关键字是关键。它确保所有线程都能看到instance
变量的最新值,同时防止指令重排序
,这对于确保双重检查锁定的正确性至关重要
那有的同学可能就要问了,为什么volatile
关键字是关键呢?如果不加呢?此时就需要分析一波了。
当执行instance = new Singleton();这行代码时,JVM可能会将对象的初始化(分配内存空间、设置默认值等)和将对象引用赋值给
instance
的操作进行重排序 CPU会执行如下指令:1.memory = allocate() 分配对象的内存空间
2.ctorInstance() 初始化对象
3.instance = memory 设置instance指向刚分配的内存
如果在单线程情况下按照上述的指令执行,那么确实不会出现问题,但是在多线程情况下,为了提高执行效率可能会打乱原有的执行顺序。
此时如果有两个线程(A,B),A到了①,B到了②此时如果出现了指令重排,按照了1,3,2执行,并且执行到了第三步骤,此时A判断不为null,就直接返回了该实例。但这时,instance还并没有完全初始化,就会出现问题。
饿汉模式
饿汉模式就比较简单一点了,没有上面的那么复杂。
饿汉模式,单例实例在类装载的时候进行创建,是线程安全的。
java
public class SingletonExample2 {
private SingletonExample2(){}
private static SingletonExample2 instance = new SingletonExample2();
public static SingletonExample2 getInstance(){
return instance;
}
}
好了,本文就介绍到这里了,后续的文章会陆陆续续的出,大家敬请期待哦~~~
提出一个小问题,大家可以在评论区讨论哦,来加深印象。
在spring中哪些使用到了单例模式呢?