懒汉式单例的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禁止指令重排序

相关推荐
小蒜学长7 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
charlie1145141917 小时前
精读 C++20 设计模式:行为型设计模式 — 访问者模式
c++·学习·设计模式·访问者模式·c++20
追逐时光者8 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友9 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧9 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧9 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧10 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
我真的是大笨蛋10 小时前
依赖倒置原则(DIP)
java·设计模式·性能优化·依赖倒置原则·设计规范
brzhang11 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang11 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构