并发编程模型详解

       现在的处理器使用写缓冲区临时保存向内存写入数据。写缓冲区可以保证指令流水线持续运行,他可以避免与处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过一批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一个内存地址的多次写,减少对内存总线的占用。虽然写缓冲区有这么多好处,但是每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读写操作的执行顺序,不一定与内存实际发生的读写顺序一致!

为了详细说明,请看下表:

并发编程模型详解

假设处理器A和处理器B按照程序的顺序并行执行内存访问,最终可能得到x=y=0的结果,具体原因如下所示:

并发编程模型详解

这里的处理器A 和处理器B可以同时把共享变量写入到自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2 B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3 B3)。当以这种时序执行时,程序就出现了x=y=0的结果。

从内存操作实际发生的顺序看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正的执行了。虽然处理器A执行内存操作顺序被重排序为A1->A2,但内存操作实际发生的顺序确是A2->A1,。此时,处理器A的内存操作数据被重排序了。(处理器B同理)

这里的关键是,由于写缓冲区仅对自己的处理器可见,他会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序。

下面是常见处理器允许的重排序的列表:

并发编程模型详解

N:表示处理器不允许操作重排序   Y:表示允许重排序

从上面的表中我们可以看到,常见的处理器都允许Store-Load重排序;常见的处理器都允许对存在数据依赖的操作做重排序。space-TSO和X86拥有相对比较强的处理器内存模型,它们仅允许对写-读操作做重排序(因为他们都使用了写缓冲区)

为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,JVM把内存屏障指令分成4类,如下所示:

并发编程模型详解

     StoreLoad Barriers 是一个 “全能型” 的屏障。它同时具有其他的3个屏障的效果。现在的多处理器大多支持该屏障(其他类型的屏障不一定被所有的处理器支持)。执行该屏障的开销会很昂贵,因为当前处理器通常把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush).