多线程 双重检查锁详解

🍓 简介:java系列技术分享(👉持续更新中...🔥)

🍓 初衷:一起学习、一起进步、坚持不懈

🍓 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正🙏

🍓 希望这篇文章对你有所帮助,欢迎点赞 👍 收藏 ⭐留言 📝

一、未加锁的单例

懒汉模式实现

java 复制代码
public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {
    }
    
    public Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }    
}

这是一个最简单的单例模式,在单线程下运转良好。但在多线程下会出现明显的问题,可能会创建多个实例。

可以看到,当两个线程同时执行时,是有可能会创建多个实例的,这很明显不符合单例的要求。

二、加锁单例

java 复制代码
public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {
    }
    
    public synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

与第一个示例唯一的区别是在方法上添加了synchronized关键字。这时,当多个线程进入该方法时,需要先获得锁才能进行执行。

  1. 通过在方法上添加synchronized关键字,看似完美的解决了多线程的问题,但却带了性能问题。

  2. 我们知道使用锁会导致额外的性能开销,对于上面的单例模式,只有第一次创建时需要锁(防止创建多个实例),但查询时是不需要锁的。

  3. 如果针对方法进行加锁,每次查询也要承担加锁的性能损耗。

三、双重检查锁

java 复制代码
public class Singleton {
    
    private static Singleton instance;
    
    private Singleton() {
    }
    
    public Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 缩小锁的范围;

  2. 锁之前先判断一下是不是null,如果不为null,说明已经实例化了,直接返回,没必要进行创建;

  3. 如果为null,进行加锁,然后再次判断是否为null。为什么要再次判断?因为一个线程判断为null之后,另外一个线程可能已经创建了对象,所以在锁定之后,需要再次核实一下,真的为null,则进行对象创建。

改进之后,既保证了线程的安全性,又避免了锁导致的性能损失。问题到此结束了吗?并没有,继续往下看

四、JVM的指令重排

在JVM当中,编译器为了性能问题,会进行指令重排。

在上述代码中new Singleton()并不是原子操作,有可能会被编译器进行重排操作。

当线程A执行完步骤赋值操作,但还未执行对象初始化。此时,线程B进来了,在第一层判断时发现Instance已经有值了(实际上还未初始化),直接返回对应的值。那么,程序在使用这个未初始化的值时,便会出现错误。

针对此问题,可在instance上添加volatile关键字,使得instance在读、写操作前后都会插入内存屏障,避免重排序。

最终,单例模式实现如下:

java 复制代码
public class Singleton {
    
    private static volatile Singleton instance;
    
    private Singleton() {
    }
    
    public Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

五、总结

  • 未加锁单例模式使用,会创建多个对象;
  • 方法上加锁,导致性能下降;
  • 代码内局部加锁,双重判断,既满足线程安全,又满足性能需求;
  • 单例模式特例:创建对象分多步,会出现指令重排现象,采用volatile进行避免指令重排;
相关推荐
wangchunyu11412 小时前
JAVA CompletableFuture异步编程实战
java·开发语言·python
paopaokaka_luck13 小时前
流浪动物救助与收养系统(DeepSeek AI对话、 ECharts多维分析、救助流程衔接、领养与回访衔接、 捐赠确认与公示、志愿者协作)
java·前端·javascript·人工智能·mysql·echarts·推荐算法
Thomas.Sir13 小时前
第9课:SpringBoot4.x Web开发基础:WebMvc自动配置、静态资源、请求参数接收
java·spring boot
程序 代码狂人13 小时前
切片表与分区表的区别(数据开发面试版)
java·大数据·开发语言·数据仓库·算法
m0_6399069414 小时前
Linux 操作系统入门:目录结构、服务排查常用命令、Shell 脚本基础
linux·运维·后端
2501_9445215914 小时前
Deepmerge 跨端三方库仓颉适配全流程
java·python·eclipse·github
Aurora1115 小时前
Jane Street OA 真题解析:Mastermind 猜密码与候选集筛选
面试
holly就是我15 小时前
AI 编程助手为什么跑偏?用开源 ccglass 看清 Prompt、工具调用和 Token 消耗
人工智能·后端
鸿蒙开发15 小时前
共享 Schema 与 SchemaFactory
前端·后端·harmonyos
用户7884773163415 小时前
小程序后端三选一:云开发、.NET WebAPI、Node,我按客户现状分了三种答案
后端