单例模式是开发中非常高频的设计模式,保证一个类在整个程序中只有唯一的一个实例对象。比如数据库连接池、线程池、配置管理对象,都适合使用单例。
单例分为饿汉、懒汉两大派系,懒汉模式会遇到多线程并发问题,如果编码不当,会创建多个实例,破坏单例特性。本文将结合多线程知识,梳理几种线程安全的单例写法,分析优缺点。
核心问题:懒汉模式,当多个线程同时调用getInstance(),同时判断instance == null,就会多次 new 对象,线程不安全。
1.饿汉模式
我们来看看代码
- 类加载的同时,创建实例
public class SingletonHungry { private static SingletonHungry instance=new SingletonHungry(); private SingletonHungry(){} public static SingletonHungry getInstance(){ return instance; } }2.懒汉模式-单线程版
- 类加载的时候不创建实例. 第一次使用的时候才创建实例.
public class SingletonLazy { private static SingletonLazy instance=null; private SingletonLazy(){} public static SingletonLazy getInstance(){ if (instance==null){ instance=new SingletonLazy(); } return instance; } }懒汉模式的优点是:类加载不创建对象, 只有代码真正用到实例的时候才去创建, 节省内存资源
❌ 致命缺点:多线程环境不安全(只能单线程使用)
多线程场景举例: 线程 A 和 线程 B 同时执行
if(instance == null)
- 线程 A 判断 instance 是 null,刚判断完,还没执行
new,CPU 切换到线程 B- 线程 B 也判断 instance 是 null,也准备执行
new- 最终两个线程都会创建对象,内存里出现两个不同 Singleton 实例,单例模式失效!
3.懒汉模式-多线程版
那我们可以加上synchronized 来改善上述的多线程环境不安全的问题
public class SingletonLazy { private static SingletonLazy instance=null; private SingletonLazy(){} public synchronized static SingletonLazy getInstance(){ if (instance==null){ instance=new SingletonLazy(); } return instance; } }4.懒汉模式-多线程版(改进)重点!!!
以下代码是在加锁的基础上做了进一步改动:
- 使用双重 if 判定, 降低锁竞争的频率
- 给instance 加上了 volatile.
public class SingletonLazy { private static volatile SingletonLazy instance=null; private SingletonLazy(){} public static SingletonLazy getInstance(){ if (instance==null){ //第一层检查 synchronized (SingletonLazy.class){ //加类锁 if (instance==null){ //第二层检查 instance=new SingletonLazy(); } } } return instance; } }1.两层 if 判断作用
- 外层
if(instance == null):实例已经初始化完成,直接 return,避免每次都进入锁竞争,提升并发性能。 - 内层
if(instance == null):多个线程同时通过外层 if,在 synchronized 处排队,一个线程创建完对象释放锁,下一个拿到锁的线程进来,再次判断,防止重复创建对象。
2.为什么必须加volatile?
instance = new SingletonDCL()这行代码不是一条原子指令,底层分为三步:
- 分配对象内存空间
- 初始化对象(执行构造方法)
- 将 instance 引用指向分配好的内存
JVM 会发生指令重排序。如果发生重排序执行顺序变成:1→3→2。 线程 A 执行到new,先分配内存,引用赋值给 instance,对象还没初始化完成。 此时线程 B 走到外层 if,发现instance != null,直接返回这个半初始化的对象,拿到一个损坏的实例,出现 bug。
volatile关键字两件事:
- 禁止指令重排序,保证 new 对象三步不会乱序;
- 保证内存可见性,一个线程修改 instance,其他线程立刻感知最新值。
⚠️没有 volatile,双重检查锁依然线程不安全!
✅优点:懒加载,只有第一次才会竞争锁, 后续调用无锁,并发性能高。