闲话(双重检查锁)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

以单例模式来说明双重检查锁

在一次上课的途中,老师突然秀了一把双重检查锁,忽然觉得心中似乎对其并不明朗,特做此篇阐述 java 的双重检查锁,特此记录,与诸君共勉。


如何实现一个安全的单例模式?

一、单例模式

定义 : 保证一个类只有一个实例,并提供一个全局的访问点;话不多说,看代码实现

public class RedissonConfig {

    private static RedissonConfig config = null;

    private RedissonConfig() {
    }

    public static RedissonConfig getInstance(){
        if (config == null) {   // 代码A
        	config = new RedissonConfig(); // 代码B
        }
        return config;
    }
}

结果 : 如下是测试方法,起 10 个线程并发的访问 getInstance() ;
在这里插入图片描述
分析: 这么看似乎也没什么问题,但是我们现在思考如下场景:
1. 线程一 , 线程二 同时走到代码A 的时候,这时候是不是会创建2个实例?看如下测试代码

public static RedissonConfig getInstance() throws InterruptedException {
        if (config == null) {
            Thread.sleep(1); // 当前线程走到这里挂起1毫秒,增大并发的现象,当前并未加锁,不存在锁占用的问题
            config = new RedissonConfig();
        }
        return config;
    }

在这里插入图片描述

二、线程安全的单例模式

1. 对方法加同步锁

public static synchronized RedissonConfig getInstance() throws InterruptedException {
        if (config == null) {  // 代码A
            Thread.sleep(1);
            config = new RedissonConfig(); // 代码B
        }
        return config;
    }

执行结果
**结果:**
分析: 我们可以看到 并发问题是解决了,但是代码确做了一些没必要的同步

  • 在多线程调用 getInstance() 方法的时候,不管当前实例是否被创建,所有的 获得单例方式都是串行的,这种同步会降低系统的并发访问性能;

优化: 我们可以按照如下代码优化(双重检查锁):

public class RedissonConfig {

    private static RedissonConfig config;

    private RedissonConfig() {
    }

    public static RedissonConfig getInstance() throws InterruptedException {
        if (config == null) {
            synchronized (RedissonConfig.class) {
                Thread.sleep(1);
                if (config == null) {
                    config = new RedissonConfig();
                }
            }
        }
        return config;
    }
}

在这里插入图片描述
代码写到这里 似乎已经没问题了,不管我是用多10个线程,还是100 个线程,测试都是同一个实例了,似乎双重检查锁也就如此,正当我有些飘飘然的时候,突然发现一个从未见过的名词 指令重排机制

三、指令重排

1. 什么是指令重排

一个类在实例化的时候,会要经历如下几步:

  • 开辟空间
  • 初始化
  • 赋值引用
    类的初始化和赋值引用是可能出现指令重排的,我们用一个例子说明 指令重排;
Student stu = new Student(); 
  • stu 可以理解为一个句柄,不能称之为一个对象
  • Student 为类声明
  • Student() 为构造方法

我们现在看看new 这个关键字发生了什么?

  • 在堆上开辟一块内存空间
  • 调用构造器student()
  • 返回空间地址并给到 stu

2. 如何防止指令重排

这个 就需要我们的 volatile 关键字出场了,volatile 关键字有如下特征:

  • 可见性, 当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存;
  • 禁止指令重排序优化 被volatile 修饰的变量 不会被jvm 指令重排;

volatile 在读的效率上 和普通变量差不多,但是在写的情况下,由于做了一些内存屏障,性能会低于普通变量,总体来说 比 synchronized 关键字性能上略高 – 摘 自 《深入理解虚拟机3》

所以最终的单例模式代码如下:

public class RedissonConfig {

    private volatile static RedissonConfig config;

    private RedissonConfig() {
    }

    public static RedissonConfig getInstance() throws InterruptedException {
        if (config == null) {
            synchronized (RedissonConfig.class) {
                if (config == null) {
                    config = new RedissonConfig();
                }
            }
        }
        return config;
    }
}

使用 volatile 修饰 私有静态变量。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值