懒汉式单例的3个坑

单例模式

Singleton

单例模式是创建型模式,通过单例模式创建的类只有一个实例存在

有时候完全只需要一个实例存在就够了,不用那么多实例存在

单例模式使用场景

  1. 频繁创建、销毁对象
  2. 创建对象耗时耗资源
  3. 工具类

懒汉式单例

懒汉式单例可以理解成懒加载,要使用时再加载

通常需要先私有化构造,通过方法获取时需要判断单例是否已经生成

java 复制代码
//懒汉式 
public class SingLeton02 {
    private SingLeton02(){

    }
    //类加载时 不初始化 所以不加final
    private static SingLeton02 INSTANCE;

    public static SingLeton02 getInstance(){
        //第一次调用这个方法时,INSTANCE为空实例化对象,之后不为空了就直接返回这个单例
        if (INSTANCE == null){
             /*try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            INSTANCE = new SingLeton02();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        //多线程测试 发现线程不安全(测不出来可以在getInstance方法中线程睡眠) 
        for (int i = 0; i < 100; i++) {
            new Thread(()-> System.out.println(SingLeton02.getInstance())).start();
        }
    }
}

线程不安全原因:当线程A进入getInstance方法判断INSTANCE为空,此时线程B也进入getInstance方法并判断INSTANCE为空,然后就造成了实例多个对象,并非单例

懒汉式加锁优化

既然懒汉式线程不安全,那就加锁

java 复制代码
//懒汉式加锁优化
public class SingLeton03 {
    private SingLeton03(){

    }

    private static SingLeton03 INSTANCE;

    //因为这里是同步方法,所以锁的是SingLeton03这个Class对象
    public synchronized static SingLeton03 getInstance(){
        if (INSTANCE == null){
            INSTANCE = new SingLeton03();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()-> System.out.println(SingLeton03.getInstance())).start();
        }
    }
}

虽然线程安全了,但是因为加了synchronized效率变低了(每次操作都要看是否加了锁等....)

懒汉式双重检测锁DCL

多线程安全

将同步方法变为同步块再优化

java 复制代码
//懒汉式双重锁优化
public class SingLeton04 {
    private SingLeton04(){

    }
	//volatile防止指令重排序
    private volatile static SingLeton04 INSTANCE;

    public  static SingLeton04 getInstance(){
        //这里的判断条件很有必要,如果没有这条判断条件,线程一进来直接抢锁降低效率
        if (INSTANCE == null){
            synchronized (SingLeton04.class){
                if (INSTANCE == null){
                    INSTANCE = new SingLeton04();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()-> System.out.println(SingLeton04.getInstance())).start();
        }
    }
}

在初始化对象的过程中可能存在指令重排序,如果此时对象发生逃逸可能会引起并发导致的一致性问题

初始化对象过程可分为以下三步:

1.分配空间 2.执行构造,初始化 3. 指向对象

多线程场景下,为防止JVM重排指令(逻辑上) 应该用volatile修饰,禁止重排指令

java 复制代码
//volatile防止重排指令
private volatile static SingLeton04 INSTANCE;

总结

本篇文章主要描述懒汉式单例的3个坑

如果懒汉式单例提供获取实例的方法没有使用同步手段那么并发请求会破坏单例,因此需要加上同步手段

如果使用同步手段(加锁)粒度太大,会导致性能问题,因此可以使用双重检测锁

由于对象创建的过程可能会进行指令重排序,如果对象发生逃逸会造成一致性问题,因此需要使用volatile禁止指令重排序

相关推荐
再吃一根胡萝卜1 分钟前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜5 分钟前
03 · 后端:FastAPI 与分层架构
面试
vx-Biye_Design22 分钟前
SSM伴侣动物伴护星小程序06330-计算机课程设计、毕业设计
spring boot·后端·elasticsearch·小程序·架构·课程设计·idea
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
面向Google编程2 小时前
Apache Iceberg Variant 类型:v3 半结构化数据不再「二选一」
大数据·后端
jason成都3 小时前
Spring WebFlux 适配达梦新方案|dm‑r2dbc:Netty 异步传输的实验性 R2DBC 驱动
java·后端·spring
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid1124 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端
小兵金林8 小时前
AI 生成代码的思考与实践
后端·ai编程