内存屏障和锁定前缀指令之间的区别

在本文内存障碍和JVM并发 !中,我被告知volatile是由不同的内存屏障指令实现的,而synchronized和atomic是通过锁定前缀指令实现的。 但我在其他一些文章中得到了以下代码:

java代码:

volatile Singleton instance = new Singleton();

汇编指令(x86):

0x01a3de1d: movb $0x0,0x1104800(%esi);

0x01a3de24: lock addl $0x0,(%esp);

那么哪一个是对的?内存障碍和锁定前缀指令有什么区别而不考虑我的英语不好?

简短的回答

锁定指令用于primefaces地执行复杂的存储器指令。
内存屏障用于命令(部分或全部)内存访问。

Java易变

Java volatile关键字保证所有线程在程序中编写时都会看到对volatile变量的更改。 volatile的唯一要点是对volatile变量的访问是完全有序的,所以如果你访问变量X然后变量Y ,两者都是volatile,那么在所有处理器的1到Y之前就可以看到对X的访问!

这需要对存储器访问进行排序,因此需要内存屏障。
IA32e上的存储器屏障可以用“栅栏”指令( mfence, lfence, sfence )或用lock指令实现。 但后一种选择只是lock的副作用而不是它的主要用途。
锁定的指令是序列化的,因此总订单。 这对于仅订购内存访问是低效的,但是有效并且它在缺少“围栏”的旧处理器中使用。

所以你看到的锁实际上是一个障碍(Linux内核也使用相同的指令)。

完整答案

上面的“复杂存储器指令”是指读 – 修改 – 写指令(在Intel命名中),这些指令在内部由三个操作组成:从内存中获取值,更改它并将其存储回来。

如果在指令期间总线未被锁定,则另一个处理器可以从存储器读取之后存储之前更改该值。

示例 x = 0

  CPU 1 CPU 2 loop: loop: inc [X] inc [x] j loop j loop 

如果每个CPU执行自己的循环10次,那么将在x中存储什么值?
你无法以决定论的方式讲述。 伪指令inc [X]必须用三个微操作实现

  CPU 1 CPU 2 loop: loop: mov r, [X] mov r, [X] inc r inc r mov [x], r mov [x], r j loop j loop 

这种情况可能已经发生:

 CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0 CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0 CPU2: mov r, [X] X is 0, CPU1 r is 1, CPU2 r is 0 CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0 CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0 CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0 CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0 CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 1 CPU2: mov [X], r X is 1, CPU1 r is 2, CPU2 r is 1 

注意X是1而不是3。
通过锁定inc指令,CPU在inc启动时断言系统总线,直到退出为止。 这会强制像这样的模式(例子)

 CPU1: mov r, [X] X is 0, CPU1 r is 0, CPU2 r is 0, CPU2 cannot use bus CPU1: inc r X is 0, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus CPU1: mov [X], r X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus CPU1: mov r, [X] X is 1, CPU1 r is 1, CPU2 r is 0, CPU2 cannot use bus CPU1: inc r X is 1, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus CPU1: mov [X], r X is 2, CPU1 r is 2, CPU2 r is 0, CPU2 cannot use bus CPU2: mov r, [X] X is 2, CPU1 r is 1, CPU2 r is 2, CPU1 cannot use bus CPU2: inc r X is 2, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus CPU2: mov [X], r X is 3, CPU1 r is 2, CPU2 r is 3, CPU1 cannot use bus 

而是使用内存屏障来命令内存访问。
处理器执行指令乱序,这意味着即使你发送CPU指令ABC它也可以执行CAB

但是,处理器需要遵守依赖关系,并且只有在不改变程序行为时才会无序执行指令。
要记住的一个非常重要的方面是指令执行和指令退出之间的区别,因为处理器保持其架构状态(程序可以看到的状态)仅对退役指令一致。 通常,程序只有在退出时才会看到指令的结果,即使它已经被执行了! 但是对于内存访问,问题略有不同,因为它们具有修改主内存的全局可见副作用,并且无法撤消!

因此,从CPU上的程序可以看出,该CPU的所有内存访问都按程序顺序进行,但是处理器并不努力保证其他处理器以相同的顺序看到内存访问! 他们看到执行顺序或最差的传播顺序,因为缓存层次结构和内存拓扑! 存储器访问的顺序与不同处理器所观察到的不同。

因此,CPU允许程序员控制如何使用障碍对存储器访问进行排序,阻止其他存储器指令(在同一CPU上)执行,直到所有前一个执行/退出/传播(这取决于体系结构的障碍)类型)。

  x = 0, y = 0 CPU 1 CPU2 mov [x], 1 loop: mov [y], 1 mov r, [y] jrz loop ;Jump if r is 0 mov s, [x] 

不需要锁。 但是没有障碍,程序之后CPU2可能为0。
这是因为在写入x之前,可以重新排序并执行CPU1的mov [y], 1写入!
从CPU 1的角度来看,没有任何改变,但对于CPU 2,订单已经改变了!

有障碍

  x = 0, y = 0 CPU 1 CPU2 mov [x], 1 loop: sync mov r, [y] mov [y], 1 jrz loop ;Jump if r is 0 mov s, [x] 

使用sync作为内存屏障伪指令。 现在写入y不能重新排序,必须等待写入x才能对CPU2可见。

事情比我的这张简单图片更精细,不同的处理器有不同的障碍和记忆排序。 不同的体系结构具有不同的缓存/内存拓扑,需要特殊处理。 抽象这并不容易,Java有一个简单的内存模型,它使生成的代码更复杂,C ++ 11有一个更精细的内存模型,可以让你更好地探索内存障碍的影响。

在阅读之前发生的抽象符号之前 –在Google上搜索常见架构(IA32e,IA64,ARM,SPARC,Power,Alpha)的内存排序问题是有用的,这样您就可以看到真正的问题是什么以及如何解决。

IA32e体系结构是一个糟糕的体系结构,因为它的轻松内存顺序确实非常强大,并且大多数问题都不会在这个体系结构上发生。 如果您有多处理器手机,则可以在ARM上进行测试。 如果您喜欢一个极端的例子,请使用Alpha架构,即使是依赖访问也会重新排序!