对单一作家的澄清

时间:2017-05-28 02:48:34

标签: java multithreading concurrency java-memory-model

我对以下陈述做了一些澄清(来源 - https://mechanical-sympathy.blogspot.in/2011/09/single-writer-principle.html):

  

'x86 / x64有一个内存模型,因此加载/存储内存操作保留了顺序,因此如果严格遵守单一编写器原则,则不需要内存屏障。

     

在x86 / x64上“根据内存模型,可以使用旧存储重新排序”,因此当多个线程跨核心改变相同数据时,需要内存障碍。 “

这是否意味着:
1.在单个内核中,加载/存储内存操作总是按顺序进行? 因此,单个核心系统上的单个编写器线程(以及多个读取器线程)不需要“同步”来解决可见性问题?
2.对于多个内核,可以通过从其他内核启动的存储重新订购负载吗? 因此,单个编写器线程(以及在其他核心上运行的多个读取器线程)不需要“同步”来解决可见性问题(因为不存在任何存储)?

因此,如果我们严格维护一个编写器 - 我们实际上可以废除在原始锁中对读取和写入使用'synchronized'的做法。 我们实际上可以完全取消'同步'吗?

2 个答案:

答案 0 :(得分:1)

在单个内核中,内存访问是按顺序还是无序进行并不重要。如果存在单个核心,它将始终感知到一致的值,因为读取请求将由保存尚未写入的数据的相同缓存提供。

然而,这与Java程序无关,因为Java语言规范的一部分Java内存模型不能为多线程程序提供这样的保证。事实上,术语“记忆障碍”根本没有出现在规范中。

您必须意识到,您编写的Java代码不是CPU将执行的x86 / x64代码。优化的本机代码与源代码不同。代码优化的一个基本部分是消除冗余读写甚至条件代码部分,假设值不会在中间发生虚假变化,这对于单线程执行总是正确的。

如果由于多线程操作而没有正确的线程安全结构而导致基础假设失效,则此优化代码将产生不一致的结果。这是规范中公认的不一致性,因为内存模型不惜一切代价强制执行一致的结果会导致性能极差。线程安全构造(如同步或易失性写入和读取)不仅告诉JVM在何处插入内存障碍(如果底层架构需要它),还告诉JVM在何处以及如何限制代码优化。

这就是为什么a)在操作可变共享状态时需要正确的线程安全结构,以及b)这些结构可能会有性能损失,即使CPU /硬件级别不需要内存屏障。

答案 1 :(得分:0)

大免责声明

我在这里写的一些事情是实际测试 - 比如重新排序,刷新等等;他们中的一些人花了很多时间阅读,我希望我把它们弄好。

所有都被重新订购,多年前不再重新订购和让您的程序以其运行方式运行的策略。只要输出没有改变,操作就会按需要重新排序。

例如:

 static int sum(int x, int y){
     x = x + 1;
     y = y + 1;
     return x + y;
 }

只要结果正确,你真的不关心完成这些操作的订单吗?

没有内存障碍(通常称为StoreLoad|StoreStore|LoadStore|LoadLoad),任何操作都可能发生变化。为了保证某些操作不move beyond a fence,已实施cpu fences。 Java生成它们的方法很少 - volatilesynchroniztionUnsafe/VarHandle(可能还有其他人,我不知道)。

基本上,当你写一个易失性的例子时,会发生这种情况:

volatile x...

[StoreStore] - inserted by the compiler
[LoadStore]
x  = 1; // volatile store
[StoreLoad] 

...

[StoreLoad]
int t = x; // volatile load
[LoadLoad]
[LoadStore]

让我们来看看这个例子的一个子集:

[StoreStore]
[LoadStore]
x = 1; // volatile store

这意味着变量的任何 StoreLoad都无法与x = 1重新排序。同样的原则适用于其他障碍。

Martin Thomson所说的是on x86 3个障碍中的3个是免费的,唯一可以发布的是:StoreLoad。它们是免费的,因为x86具有强大的内存模型,这意味着其他操作默认情况下不会重新排序。在其他cpu上,其中一些操作也很便宜(如果我错误ARM那就是lwsync - 轻量级同步;名称应该是自解释的)。

此外,CPU和缓存之间还有一点缓冲区 - 称为Store Buffer。当您向变量写入内容时,它不会直接转到缓存中。它转到那个缓冲区。当它已满(或被强制通过StoreLoad耗尽)时,它会将写入放入缓存 - 并且由cache coherency protocol来同步所有缓存中的数据。

马丁说的是,如果你有多个作家,你必须多次发出StoreLoad - 因此它很昂贵。如果你有一个单独的作家,你不需要。缓冲区已满时将被耗尽。什么时候发生?好吧有时,理论上可能永远不会,实际上很快。

一些很棒的资源(这些资源有时让我整晚都没有睡觉,所以要小心!):

每次写入构造函数中的最终变量时,这些StoreStore btw:

 private final int i ;

 public MyObj(int i){
     this.i = i;
      // StoreStore here 
}

LazySet

Shipilev Volatile

And my all time favorite!