单例模式、指令重排序、锁、有序性

今天在回顾单例模式时,我们都知道懒汉式单例中有一种叫做双重检查锁的单例模式。

我们来看下下面的代码有没有问题:

这段代码我们可以看到,即优化了性能,在多线程情况下,如果实例不为空了,则直接返回了。这样就不用等待排队获取锁了。

同时也保证了线程的安全性,即全程只会出现一个实例。

但是真的没有问题了吗?我们来分析一下:

在执行到 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();这条语句时,这里是会发生一个指令重排序的问题的。什么是指令重排序呢?

正常的我们创建一个对象,对于底层来说是: a. 内存分配 b. 初始化 c. 返回对象引用

但是由于指令重排序:我们的指令执行过程可能会成为 a. 内存分配 b. 返回对象引用 c. 初始化

如果是我们指令重排序的这种结果。那我们上面的代码就会产生问题了。

即:假设第一个线程执行到了 lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton(); 这条语句,但是只是执行了指令中的 a 、 b 即初始化还没有完成,但此时 lazyDoubleCheckSingleton这个引用已经不为空了,此时第二个线程过来,在外层判断发现lazyDoubleCheckSingleton不等于空,就直接返回了,但此时返回的对象很明显是个半成品,还没有初始化。因此就会导致产生不可预估的错误。

具体为什么会产生指令重排序,或者指令重排序的详细概念请看https://blog.csdn.net/weixin_37841366/article/details/113086438

以上是问题的背景。

但是我的疑问是为什么会产生指令重排序呢?我印象中学习过的JUC不是说加锁:即Synchronized是可以保证 有序性、可见性、原子性的吗?这个赋值创建的语句这不是在Synchronized代码块里面呢吗?怎么会有指令重排序的问题呢?

下面我们就来分析一下为什么会产生这样的问题吧~

首先我们需要先好好理解一下加锁保证的有序性和volatile关键字防止指令重排序保证的有序性的区别。

首先我们需要明确一点:那就是加锁是无法防止指令重排序的。那为什么说他能够保证有序性呢?

我们需要了解一个语义:

编译器和处理器必须遵守as-if-serial语义,即不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。

因为Synchronized块中的代码相当于是单线程执行的,而因为这个语义的存在,单线程执行的执行结果是保证不能被改变的,因此Synchronized代码块包裹的代码是有序的代码。这里的有序指的是宏观的有序。

但我们的双检查单例为什么靠Synchronized锁做不到保证有序呢?

因为我们在代码块外面的那个if判断是不受Synchronized控制的。Synchronized的内部是有序了。但是外部依旧无序。

因此上面的代码我们需要添加volatile关键字防止指令重排序。让其保证微观上的强有序性。

相关推荐
攻城狮7号7 小时前
【第三节】C++设计模式(创建型模式)-单例模式
c++·单例模式·设计模式
Niuguangshuo9 小时前
Python 单例模式笔记
笔记·python·单例模式
coooliang14 小时前
Flutter 中的单例模式
javascript·flutter·单例模式
鄃鳕1 天前
单例模式【C++设计模式】
c++·单例模式·设计模式
yuanpan2 天前
23种设计模式之《单例模式(Singleton)》在c#中的应用及理解
单例模式·设计模式·c#
小突突突2 天前
总结单例模式的写法(在线程安全的情况下)
java·开发语言·后端·单例模式·java-ee
老朋友此林2 天前
详解单例模式、模板方法及项目和源码应用
java·开发语言·单例模式
one客3 天前
设计模式 - 单例模式
c++·单例模式·设计模式
It_BeeCoder3 天前
Java:单例模式(Singleton Pattern)及实现方式
java·单例模式·程序员
S-X-S3 天前
八种单例模式详解
单例模式