双重检查锁单例与内存屏障分析

10 篇文章 0 订阅
单例(双重检查锁)
public class DoubleCheckLockSingleton {
    
    private static DoubleCheckLockSingleton instance = null;

    private DoubleCheckLockSingleton(){

    }
    public static DoubleCheckLockSingleton getInstance() {
    	//当程序顺序执行的时候,如果不进行判空,每一个线程都会先去获得当前类的类锁,而其他线程都进入阻塞状态。
        if (instance == null){
                        /**
             * 10 monitorenter
             * 11 getstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//获取静态变量
             * 14 ifnonnull 27 (+13)//判空
             * 17 new #3 <DoubleCheckLockSingleton>//如果等于null,创建对象
             * 20 dup
             * 21 invokespecial #4 <DoubleCheckLockSingleton.<init> : ()V>//对象的初始化
             * 24 putstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//给变量赋值
             * 27 aload_0
             * 28 monitorexit
             */
            synchronized (DoubleCheckLockSingleton.class){
                if (instance == null){
                    instance = new DoubleCheckLockSingleton();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        DoubleCheckLockSingleton doubleCheckLockSingleton = DoubleCheckLockSingleton.getInstance();
    }
}

​ 程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。注意上面的代码中属性是没有加volatile关键字的,是有可能发生指令重排的,

对于JVM来说:instance=new DoubleCheckLockSingleton();是做了以下的三件事的:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

对面上面的这段代码来说

JVM执行到instance=new DoubleCheckLockSingleton()时,可能的实行顺序就是1–>2–>3或1–>3–2,再多线程的情况下上面的两种情况都是有可能出现的。如果是前者自然是没有什么问题,但是如果出现后者那么就没有按照我们希望的循序执行。这样会出现已经重复创建对象的情况,也就没有达到我们单例的设计原则。

从更底层的方式来思考双锁出现的问题

这里我们使用idea插件jclasslib来分析(字节码阅读器)

主要看下面这部分:

 /**
 * 10 monitorenter
 * 11 getstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//获取静态变量
 * 14 ifnonnull 27 (+13)//判空
 * 17 new #3 <DoubleCheckLockSingleton>//如果等于null,创建对象
 * 20 dup
 * 21 invokespecial #4 <DoubleCheckLockSingleton.<init> : ()V>//对象的初始化
 * 24 putstatic #2 <DoubleCheckLockSingleton.instance : LDoubleCheckLockSingleton;>//给变量赋值
 * 27 aload_0
 * 28 monitorexit
 */

​ 高并发下,若全按照上面的顺序来执行就和我们期望的一样了,但是不然。编号21和24的顺序是不确定的,也就是存在半初始化问题,也就是24执行在前,21后。首先在锁机制下,当一条线程执行锁中的代码时,其他需要锁的线程全部就被挡在了锁的外部等待。该线程进入锁内,在对象没有真正的初始化时就对变量进行了赋值(此时对象里面的值都是堆中jvm给的默认值),外部的线程执行到 if (instance == null),发现不为空,立即返回该对象。那么线程就拿到了一个错误的对象

单例双重检查锁问题的解决

添加volatile

private static volatile DoubleCheckLockSingleton instance = null;

分析之前先补充知识:

volatile 的底层实现是通过插入内存屏障实现(C++,汇编实现)。
  • 每个 volatile 写操作前面插入一个 StoreStore 屏障
  • 每个 volatile 写操作后面插入一个 StoreLoad 屏障
  • 每个 volatile 读操作后面插入一个 LoadLoad 屏障
  • 每个 volatile 读操作后面插入一个 LoadStore 屏障
内存屏障
  • StoreStore 屏障可以保证在 volatile 写之前,其前面的所有普通写操作都已经刷新到主内存中
  • StoreLoad 屏障的作用是避免 volatile 写与后面可能有的 volatile 读/写操作重排序
  • LoadLoad 屏障用来禁止处理器把上面的 volatile 读与下面的普通读重排序
  • LoadStore 屏障用来禁止处理器把上面的 volatile 读与下面的普通写重排序

在这里插入图片描述

JVM规定volatile需要实现的内存屏障

在这里插入图片描述

分析

​ 首先在锁机制下,当一条线程执行锁中的代码时,其他需要锁的线程全部就被挡在了锁的外部等待。该线程进入锁内,执行到instance = new DoubleCheckLockSingleton(); **发现instance变量是被volatile修饰的,于是在该条语句的前后分别添加内存屏障。**防止了指令的重排。这样就解决了双锁检查单例的问题。

参考:
1,https://www.cnblogs.com/webor2006/p/12598378.html
2,https://www.cnblogs.com/yaowen/p/11240540.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值