大厂面试(五)DCL到底需不需要加volatile?—美团

DCL到底需不需要加volatile?

DCL:(Double Check Lock),双重判断锁, 要知道DCL的由来,先从单例模式说起。

单例模式——饿汉式

/**
 * 单例模式 -- 饿汉式
 */
public class Singleton01 {
    private static final Singleton01 INSTANCE = new Singleton01();
    private Singleton01 (){
    }
    public static Singleton01 getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        Singleton01 instance1 = getInstance();
        Singleton01 instance2 = getInstance();
        System.out.println(instance1 == instance2);
    }
}

饿汉式就是甭管三七二十一,我上来就先把这个对象new出来,构造方法是private的,别人创建不了,通过一个静态的getInstance()方法获取实例,这样能够保证我每次获取的对象都是同一个,这是单例最简单的写法。

这时候有人说了,你这写的不咋地啊,我还没用你这个对象呢,你就给我new出来了,太浪费空间了。

单例模式——懒汉式

/**
 * 单例模式--懒汉式
 */
public class Singleton02 {
    private static Singleton02 INSTANCE;
    private Singleton02(){
    }
    public static Singleton02 getInstance(){
        if (INSTANCE == null) {
            try {
              	// 业务代码
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }

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

饱汉式是什么时候使用,我什么时候再给你创建,对象不为空我就直接返回。

这时候又有人说了,你这不对呀,在多线程的情况下是有可能创建多个对象的,什么意思呢?比如有两个线程,第一个线程进来判断INSTANCE是否为空,第一次进来肯定为空,继续执行,在执行业务代码还没有new对象的时候,第二个线程进来,判断INSTANCE是否为空,依然为空,继续往下执行,这时候线程一new了一个对象返回,线程二也继续执行new了一个对象返回,一共创建了两个对象,从上面的程序也能验证出来,hashCode不一致说明不是同一个对象。

单例模式——升级版

/**
 * 单例模式--升级版
 */
public class Singleton03 {
    private static Singleton03 INSTANCE;
    private Singleton03(){
    }
    public static synchronized Singleton03 getInstance(){
        if (INSTANCE == null) {
            try {
              	// 业务代码
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            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().hashCode())).start();
        }
    }
}

我在方法上加了一把锁,每个线程进来必须先拿到这把锁才能执行里面的代码,同一时刻,只能有一个线程在执行,执行完的时候一定能new出一个对象,释放锁后,第二个线程才能进来,判断的时候这个对象一定不为空,在多线程的情况下能够保证对象时唯一的。

这时候又有人问了,你这锁加到方法上了,锁的粒度太大了,我只想在需要加锁的地方加锁行不行。

单例模式——降低锁粒度

/**
 * 单例模式--降低锁粒度
 */
public class Singleton04 {
    private static Singleton04 INSTANCE;
    private Singleton04(){
    }
    public static Singleton04 getInstance(){
        // 业务代码
        if (INSTANCE == null) {
            synchronized (Singleton04.class) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                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().hashCode())).start();
        }
    }
}

看下上面这段代码,先判断对象是否为空,为空你再上锁,这个能解决多线程下的数据不一致性问题吗?看下运行结果

很显然,还是不能保证多线程下的数据不一致性问题,怎么回事呢?

当线程一进入方法判断对象是否为空,为空,然后第一个线程停了,第二线程进来判断是否为空,为空,继续执行,new了一个对象后返回释放锁,线程一拿到锁后继续执行,又new了一个对象。所以终于诞生了美团问的这个写法,DCL

单例模式——DCL(DOUBLE CHECK LOCK)

/**
 * 单例模式--DCL
 */
public class Singleton05 {
    private static volatile Singleton05 INSTANCE;
    private Singleton05(){
    }
    public static Singleton05 getInstance(){
        // 业务代码
        if (INSTANCE == null) {
            synchronized (Singleton05.class) {
                if (INSTANCE == null) {  // 双重验证
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton05();
                }
            }
        }
        return INSTANCE;
    }

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

以上这段代码就是DCL,能够保证多线程下的数据不一致性问题,为什么?

线程一进入到这段代码,判断是否为空,为空,这时候线程一进入到if方法,线程一停了,线程二进来,判断是否为空,为空,继续执行,拿到锁,再判断为空吗?为空,new对象返回,释放锁,线程一继续执行,拿到锁,判断对象为空吗?不为空!返回。

这里又有人会问,你代码里第一个判断有啥用?上来直接上锁不就完了嘛!假如有1万个线程进来,如果没有第一层判断,那么这1万个线程都要去获取锁,效率太低!如果有第一层判断,有一个线程执行完了,其他线程我就不需要再去获取锁了,这就是效率问题。

但这不是美团问的不是这个,而是问你,**DCL需要加volatile吗?**答案是肯定的,一定要加,为什么?

这里说的就是上篇文章说的volatile的第二个作用,禁止指令重排序

说到禁止指令重排序又得先说下对象的创建过程(半初始化问题)。

假设我有一个类,里面有个成员变量m,值为8

public class T {
  	int m = 8;
    public static void main(String[] args) {
        T t = new T();
    }
}

请问,这个对象的创建过程是?

idea–>点击view–>Show Bytecode

1. NEW com/zyj/study/algorithm/volatil/T
2. INVOKESPECIAL com/zyj/study/algorithm/volatil/T.<init> ()V
3. ASTORE 1
  1. NEW: 在内存中申请一块空间

    当我new这个对象的时候,内存中已经有了一块空间,成员变量m也有了,但是m的值此时是8吗?不是,它的值是int默认的值0,如果类型是boolean,那它的值就是false,如果类型是引用类型,那么它的值是null。

  2. INVOKESPECIAL:调用特殊的方法(T的构造方法)

    调用构造方法之后,才会给m赋值,这时候m的值才等于8。

  3. ASTORE 1:建立关联

    将t指向已经调用完构造方法的空间。

在对象创建的过程中,成员变量的值是在调用构造方法时才给的,所以说有个半初始化的过程。

那么现在我们结合DCL的代码看下对象的初始化过程。

当一个线程进来的时候,判断对象是否为空?肯定为空,因为还没创建呢,往下执行,拿到锁,继续往下执行,再次判断是否为空?为空,往下执行,在new对象的时候,对象有个半初始化的一个状态,在执行完new的时候,分配了一块空间,成员变量是引用类型那么它的值为null,就在此时,invokespecialastore 1发生了指令重排序,直接将INSTANCE指向了初始化一半还没有调用构造方法的内存空间,这时候第二个线程进来了,判断对象为空吗?不为空,为啥?因为它指向了一个半初始化的一个对象嘛!既然不为空,我就直接返回了这个初始化一半的对象。

所以说,这段代码要不要加volatile?必须加!加了volatile的这块内存,对于它的读写访问不可以重排序!

那么有的同学会问,为什么是这两条指令发生指令重排序?

什么样的指令可以发生重排序:两条指令互不影响(无关)。

什么样的指令不可以发生重排序:

​ JVM规定:8种happens-before不可以发生指令重排序,不包括刚才的那两条,所以那两条可以发生重排序

那么volatile是如何禁止指令重排序的呢?敬请期待下篇文章《内存屏障》。

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YoungJ5788

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值