指令重排 内存屏障

instance = new Single()这句,这并非是一个原子操作,在 JVM 中做了下面 3 件事情。

  1. 给 instance 分配堆内存(Single 对象)

  2. 调用 Single的构造函数来初始化成员变量,形成实例

  3. 将instance 指针指向分配的内存空间(执行完这步 singleton才是非 null了)。

简单说即,开辟堆空间,构造,指向堆空间。

  正常执行顺序:1->2->3,由于2和3没有依赖性(1和3有依赖性),可能发生指令重排,可能的执行顺序为:1->3->2。

当1,3执行后,instnce指针是不为null了,此时,另一个线程执行 if(instance == null) 就会判断是非空直接返回,而此时,Single的构造还可能未执行,会引发严重数据错误!!!!

解决方法(volatile)

volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。

内存屏障由来

对于CPU的写,目前主流策略有两种:

1、write back:即CPU向内存写数据时,先把真实数据放入store buffer中,待到某个合适的时间点,CPU才会将store buffer中的数据刷到内存中,而且这两个操作是异步的。这在多线程环境中,有些情况下是可以接受的,但是有些情况是不可接受的,为了让程序员有能力根据业务需要达到同步完成,就设计了内存屏障。关于内存屏障,后面会细讲。

2、write through:即CPU向内存写数据时,同步完成写store buffer与内存。

当前CPU大多数采用的是write back策略。可能有童鞋要问了:为什么呢?因为大多数情况下,CPU异步完成写内存产生的部分延迟是可以接受的,而且这个延迟极短。只有在多线程环境下需要严格保证内存可见等极少数特殊情况下才需要保证CPU的写在外界看来是同步完成的,需要借助CPU提供的内存屏障实现。如果直接采用策略2:write through,那每次写内存都需要等待数据刷入内存,极大影响了CPU的执行效率。

内存屏障实现

为什么要插入屏障?本质是业务层面不能接受写store buffer与刷回内存两个异步操作产生的哪怕是极少的延迟,即内存可见性的要求极高

内存屏障到底是什么?内存屏障什么都不是,它只是一个抽象概念,就像OOP。如果这样说你不理解,那你把他理解成一堵墙,这堵墙正面与反面的指令无法被CPU乱序执行及这堵墙正面与反面的读写操作需有序执行。

CPU提供了三个汇编指令串行化运行读写指令达到实现保证读写有序性的目的:

SFENCE:在该指令前的写操作必须在该指令后的写操作前完成

LFENCE:在该指令前的读操作必须在该指令后的读操作前完成

MFENCE:在该指令前的读写操作必须在该指令后的读写操作前完成

何谓串行化?你可以理解成CPU把读、写、读写请求放入了一个队列,按照先进先出的顺序执行下去;何谓读操作完成,即CPU执行一次读操作,把值读到寄存器中;何谓写操作完成,即CPU执行一次写操作,数据刷到内存中

Java的volatile在实现层面用的不是fence族屏障,而是lock。lock是如何实现屏障效果的呢?JVM为什么不用fence族呢?

。。。

(0)

相关推荐