目录
1.单例模式的定义
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。单例模式常用于管理共享资源(如数据库连接池、线程池、配置对象等)。
其中的设计模式指的是解决软件设计中常见问题的可复用方案 ,是面向对象编程的经验总结。它们分为 创建型 、结构型 和 行为型 三大类。简要说就是类似于框架,是大佬们提供的可供使用的一种"公式"。
2.单例模式的实现方式
1.饿汉模式
java
public class Singleton {
private static final Singleton INSTANCE = new Singleton(); // 类加载时创建
private Singleton() {} // 私有构造方法
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉模式指的是在方法加载的同时就初始化实例(不管你用不用,我先开头就把实例创建了)。
优点:简单,线程安全(JVM保证类加载时的线程安全)。
缺点:即便不使用也会照常创建方法,可能会造成资源浪费。
2.懒汉模式
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式指的是方法加载时不先创建实例,等到真正用到的时候再创建。
优点:资源利用率高,避免不必要的实例创建。
缺点:带来了线程不安全的问题。
(1)线程不安全的问题怎么解决?
由于懒汉模式带来的线程不安全问题,我们要在代码中加入锁,也就是加入synchronized关键字。
(2)直接对整个getInstance方法代码块加锁吗?
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
像这样直接对整个getInstance方法修饰synchronized关键字固然可以解决线程安全问题。
但是由于整个getInstance方法只有在第一次创建实例时会进入if语句,存在线程安全隐患 ;其他情况下 ,代码会略过if语句,直接return,也就不存在线程安全问题了。
直接对整个方法加锁会导致不必要的开销变大 (加锁的开销),资源利用率降低。
(3)那对if语句加锁不就行了吗?
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
synchronized(Singleten.calss){
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这样写的代码,虽然没有对整个方法加锁,但是在代码执行过程中,不管有没有创建完成实例,都会对if语句加锁,然后再判断if语句。
这依然会导致性能低下。
(4)那在if语句内部加锁可以吗?
java
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleten.calss){
instance = new Singleton();
}
}
return instance;
}
}
这样虽然解决了开销问题,但是在进入if语句之后,加锁之前 ,如果线程被调度走(抢走),其他线程创建了实例,代码继续执行,这时,锁才刚被加上。
这就会导致多次创建实例。
(5)应用双重if语句判断
java
public class Singleton {
private static volatile Singleton instance; // ✅ volatile 禁止指令重排序
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton();
}
}
}
return instance;
}
}
第一次if语句判断是否是第一次创建实例,减少全部加锁的不必要开销。
第二次if语句判断加锁完成后其他线程是否创建了实例,防止多次创建实例。
(6)加入volatile防止指令重排序
指令重排序 是编译器和处理器为了优化程序性能,在不改变程序语义 (单线程环境下最终执行结果)的前提下,对指令的执行顺序进行重新排列的一种优化手段。不过在多线程环境中,指令重排序可能会引发一些难以调试的问题。
因为实例的创建不是原子的 ,instance = new Singleton()分为三步:1.分配内存空间,2.初始化对象,3.将instance指向内存地址。
所以如果触发编译器的指令重排序,就有可能打乱instance = new Singleton()的三步的顺序,造成线程不安全。
java
public class Singleton {
private static volatile Singleton instance; // ⚠️ 必须加 volatile
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton();
}
}
}
return instance;
}
}
3.相关的面试题
(1)为什么需要双重检查?
第一重if检查是否是第一次创建实例,这样可以保证在以后得代码执行过程中直接跳过if语句代码块,减少不必要的开销(加锁)。
第二重if检查进入第一重if语句之后,加锁之前,线程是否有被调度走,实例是否已经被创建完毕。
避免多次创建实例。
(2)为什么需要加volatile修饰?
防止指令重排序。
创建实例的过程不是原子的,instance = new Singleton()分为三步:
(1)分配内存空间
(2)初始化对象
(3)istance指向内存空间
指令重排序可能会打乱这三步,导致其他线程拿到未创建的实例或者多次创建实例。
(3)为什么静态内部类不需要加volatile修饰?
java
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton(); // 类加载时创建
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE; // 第一次调用时加载内部类
}
}
因为JVM保证了线程安全,并且初始化是原子的。
(4)单例模式的缺点是什么?
(1)难以拓展,通常不允许子类化。
(2)隐藏了依赖关系
(3)长期持有对象可能增加内存压力。