多线程与高并发——DCL

DCL双重检查锁

DCL全称为Double Check Lock,中文为双重检查锁,

DCL的简单实现

/**
 * DLC单例模式的双重检查锁
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2021/1/9
 */
public class DlcDoubleCheckLock {
    private static volatile DlcDoubleCheckLock dlcDoubleCheckLock = null;

    /**
     * 必须要重新实现一个无参构造方法,因为这个bean是需要我们自己代码实例化,而不能让用户实例化
     */
    private DlcDoubleCheckLock() {
    }
    public static DlcDoubleCheckLock getInstance() {
        if (dlcDoubleCheckLock == null) {
            synchronized (DlcDoubleCheckLock.class) {
                if (dlcDoubleCheckLock == null) {
                    dlcDoubleCheckLock = new DlcDoubleCheckLock();
                }
            }
        }
        return dlcDoubleCheckLock;
    }
}

volatile

保证线程可见性(线程之间的变量可被重读到)

  • MESI
  • 缓存一致性协议

禁止指令重排序(CPU)

  • 应用场景——DCL单例

    • 双重检查锁

      • 是否需要增加volatile?

        需要增加,因为可能会有指令重排序的问题

volatile的缺点

无法保证原子性,即不能替代synchronized

可从如下代码中看出来

/**
 * 测试Volatile不具备原子性,无法替代synchronized,只能保证线程的可见性
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2021/1/9
 */
public class VolatileTest {
    volatile int count = 0;

    void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        List<Thread> threads = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(volatileTest::m, "thread-" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(volatileTest.count);
        // 可能是26421
        // 可能是24113
        // 也可能是其他各类答案
    }
}

原因分析:

首先代码中如果不加入volatile,那么各个线程中,对于count这个变量肯定是不知道的,答案肯定是不正确的,如果加入了volatile,以3个线程为例,第一个线程++完成后为1,丢回了内存中,线程2,3都能拿到加完后的值1,在自身线程中++后又丢回内存中,可以发现,此时count为2,而不是期盼的3,所以可以说Volatile不具备原子性,无法替代synchronized。

如何解决这个问题?

只需要加入synchronized即可,代码如下

/**
 * 测试Volatile不具备原子性,无法替代synchronized,只能保证线程的可见性
 * @author hanhuafeng
 * @version V1.0
 * @description
 * @date 2021/1/9
 */
public class VolatileTest {
    volatile int count = 0;

    synchronized void m() {
        for (int i = 0; i < 10000; i++) {
            count++;
        }
    }

    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        List<Thread> threads = new LinkedList<>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(volatileTest::m, "thread-" + i));
        }
        threads.forEach(Thread::start);
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(volatileTest.count);
        // 可能是26421
        // 可能是24113
        // 也可能是其他各类答案
    }
}

synchronized

关于synchronized可以看我的这篇文章 多线程与高并发——synchronized

DCL在面试中可能被问到的问题:

volatile是否需要加?

答:需要加,在计算机指令中,, CPU和编译器为了提升程序的执行效率, 通常会按照一定的规则对指令进行优化, 如果两条指令互不依赖, 有可能它们执行的顺序并不是源代码编写的顺序,这可以称之为指令重排序,比如正常情况下 instance = new Instance()可以分成三步:

  1. 分配对象内存空间(此时值为默认值)

  2. 初始化对象成员变量

  3. 设置instance(栈内存中)指向刚刚分配的内存地址, 此时instance != null (重点)

因为2和3两步不存在数据上的依赖关系, 即在单线程的情况下, 无论2和3谁先执行, 都不影响最终的结果, 所以在程序编译时, 有可能它的顺序就变成了

  1. 分配对象内存空间(此时值为默认值)

  2. 设置instance(栈内存中)指向刚刚分配的内存地址, 此时instance != null (重点)

  3. 初始化对象成员变量

假设此时第一个线程正好被指令重排序,且执行到了第二步——设置instance(栈内存中)指向刚刚分配的内存地址,此时的instance != null,那么第二个线程取的时候,他所拿到的,就是刚刚分配了默认值的对象,而不是我最终想要的结果。

那么加了volatile之后会有什么不同呢? volatile有三个特点:

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值