🌈🌈🌈🌈🌈🌈🌈🌈
欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
🍁🍁🍁🍁🍁🍁🍁🍁
这里说一个在 JIT 动态编译时,经典的指令重排现象,双端检锁
时发生指令重排可能导致的错误
首先,双端检锁是用于构造单例对象的,如下:
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
//第一次校验单例对象是否为空
if (INSTANCE == null) {
//同步代码块
synchronized (Singleton.class) {
//第二次校验单例对象是否为空
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> System.out.println(Singleton.getInstance().hashCode())).start();
}
}
}
从字节码层面来看上述代码
0 getstatic #2 <com/qy/nettychat/Volatile/Demo1.INSTANCE : Lcom/qy/nettychat/Volatile/Demo1;>
3 ifnonnull 37 (+34)
6 ldc #3 <com/qy/nettychat/Volatile/Demo1>
8 dup
9 astore_0
10 monitorenter
11 getstatic #2 <com/qy/nettychat/Volatile/Demo1.INSTANCE : Lcom/qy/nettychat/Volatile/Demo1;>
14 ifnonnull 27 (+13)
17 new #3 <com/qy/nettychat/Volatile/Demo1>
20 dup
21 invokespecial #4 <com/qy/nettychat/Volatile/Demo1.<init> : ()V>
24 putstatic #2 <com/qy/nettychat/Volatile/Demo1.INSTANCE : Lcom/qy/nettychat/Volatile/Demo1;>
27 aload_0
28 monitorexit
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <com/qy/nettychat/Volatile/Demo1.INSTANCE : Lcom/qy/nettychat/Volatile/Demo1;>
40 areturn
其中双端检锁(DCL)部分字节码如下
17 new #3 <com/qy/nettychat/Volatile/Demo1>
20 dup
21 invokespecial #4 <com/qy/nettychat/Volatile/Demo1.<init> : ()V>
24 putstatic #2 <com/qy/nettychat/Volatile/Demo1.INSTANCE : Lcom/qy/nettychat/Volatile/Demo1;>
在单线程下 putstatic
和 invokespecial
进行指令重排,可以提高效率;在多线程下,指令重排可能会出现意想不到的结果
dup
指令之后,为了加快程序执行效率,跳过构造方法的指令(invokespecial
) ,直接执行putstatic
指令,然后再将操作数栈上剩下的引用来执行invokespecial
。单线程情况下JVM任何打乱invokespecial
和putstatic
执行顺序并不会影响程序执行的正确性。getInstance
会执行到if(INSTANCE==NULL)
,此时会拿到一个尚未初始化完成的对象,那么使用未初始化完成的对象时可能会发生错误。这个内容可以作为扩展了解!
指令乱序机制是现代处理器中用于提升性能的一种技术
指令乱序的意思时,处理器
不会按照程序中指令的顺序来严格顺序执行,而是会动态地调整指令地顺序,哪些指令先就绪,就先执行哪些指令,之后将每个指令的执行结果放到一个 重排序处理器
中,重排序处理器把各个指令的结果按照代码顺序应用到主内存或者写缓冲器里
指令乱序机制可能造成数据一致性的问题,因此处理器提供了 内存屏障
和 同步原语(volatile、synchronized 等)
来保证在需要的时候,指令的执行顺序可以被保证同步,防止指令乱序执行
内存重排序
造成的视觉假象这里讲一下高速缓存和写缓冲器的 内存重排序
造成的视觉假象:
处理器会将数据写入到写缓冲器中,这个过程就是 store;从高速缓存里读数据,这个过程就是 load,对于处理器来说,它的重排处理器是按照顺序来 load 和 stroe 的,但是如果在写缓冲器中发生了内存层面的指令重排序,就会导致其他处理器认为当前重排序后的指令顺序发生了变化
举个例子:比如现在有两个写操作 W1 和 W2,处理器先执行了 W1 再执行了 W2,写入到了 写缓冲器
中,而写缓冲可能为了提升性能,先将 W2 操作的数据写入到高速缓存中,再将 W1 操作的数据写入到高速缓存中,这样 W2 操作的结果先写入到 高速缓存
中后,会 先被其他处理器感知到
,那么其他处理器就会误认为 W2 操作是先于 W1 操作执行的,这个就是 重排序造成的视觉假象
!
整个过程如下图,处理器 1 先执行 W1 再执行 W2,结果写缓冲器重排序后,将 W2 操作排在了前边:
这个内存重排序,有4种可能性:
接下来举一个具体的例子:
对于下边的代码,在硬件层面上可能多个线程并行地调度到不同地处理器上执行,通过并行执行来提高性能,假如 处理器 0
和 处理器 1
同时来执行下边代码,如果处理器 0 的写缓冲器为了提高性能,进行了 内存重排序
,先将 loaded = true
的结果更新到 高速缓存
,再去更新 loadConfig()
的执行结果,那么如果处理器 0 刚更新完 loaded 的值,还没来得及更新 loadConfig 的值,此时 resource
还是 null,处理器 1 发现 loaded 为 true 了,直接调用 resource.execute()
方法,那么就会出现空指针的问题,这就是内存重排序可能会带来的问题
:
Config config = null;
Boolean loaded = false;
// 处理器 0
resource = loadConfig();
loaded = true;
// 处理器 1
while (!loaded) {
try {
Thread.sleep(1000);
} catch (Exception e) {
...
}
}
resource.execute();
为了容易理解 指令重排如何造成空指针问题
,我这里也画了一张时间线图: