Java中的内存屏障行为

时间:2014-06-28 16:58:31

标签: java multithreading memory-barriers java-memory-model

在阅读了更多博客/文章等之后,我现在对内存屏障之前/之后的加载/存储行为感到困惑。

以下是Doug Lea在他关于JMM的一篇澄清文章中的两个引用,它们都是非常直接的:

  1. 当线程A写入易失性字段f时,线程A可见的任何内容在读取f时都会对线程B可见。
  2. 请注意,两个线程都必须访问相同的volatile变量才能正确设置before-before关系。情况并非如此,当线程A写入易失性字段f时,线程A可见的所有内容在读取易失性字段g后都会对线程B可见。
  3. 但是当我看到关于记忆障碍的另一个blog时,我得到了这些:

    1. 存储屏障,x86上的“sfence”指令强制屏障之前的所有存储指令发生在屏障之前,并将存储缓冲区刷新为缓存以供发布它的CPU。
    2. x86上的加载屏障“lfence”指令强制屏障后的所有加载指令发生在屏障之后,然后等待加载缓冲区为该CPU耗尽。
    3. 对我而言,Doug Lea的澄清比另一个更严格:基本上,这意味着如果负载屏障和存储屏障位于不同的监视器上,则无法保证数据的一致性。但后者意味着即使屏障位于不同的监视器上,数据的一致性也会得到保证。我不确定我是否正确理解这两个,而且我也不确定它们中哪一个是正确的。

      考虑以下代码:

        public class MemoryBarrier {
          volatile int i = 1, j = 2;
          int x;
      
          public void write() {
            x = 14; //W01
            i = 3;  //W02
          }
      
          public void read1() {
            if (i == 3) {  //R11
              if (x == 14) //R12
                System.out.println("Foo");
              else
                System.out.println("Bar");
            }
          }
      
          public void read2() {
            if (j == 2) {  //R21
              if (x == 14) //R22
                System.out.println("Foo");
              else
                System.out.println("Bar");
            }
          }
        }
      

      假设我们有1个写线程TW1首先调用MemoryBarrier的write()方法,然后我们有2个读取器线程TR1和TR2调用MemoryBarrier的read1()和read2()方法。考虑这个程序在CPU上运行,它不保留排序(x86 DO保留这种情况的排序而不是这种情况),根据内存模型,在W01之间会有一个StoreStore屏障(让我们说SB1) / W02,以及R11 / R12和R21 / R22之间的2个LoadLoad屏障(让我们说RB1和RB2)。

      1. 由于SB1和RB1在同一台显示器上 i ,因此调用 read1 的线程TR1应始终在x上看到14,同时" Foo"总是打印出来。
      2. SB1和RB2在不同的监视器上,如果Doug Lea是正确的,则线程TR2将不能保证在x上看到14,这意味着" Bar"偶尔可以打印。但如果内存屏障像blog中描述的Martin Thompson那样运行,Store屏障会将所有数据推送到主内存,Load barrier会将所有数据从主内存提取到缓存/缓冲区,然后TR2也会保证看到14 on x。
      3. 我不确定哪一个是正确的,或者两者都是正确的,但Martin Thompson所描述的仅适用于x86架构。 JMM不保证对TR的可见变化对于TR2是可见的,但x86实现确实如此。

        感谢〜

2 个答案:

答案 0 :(得分:15)

Doug Lea是对的。您可以在 Java语言规范§17.4.4部分找到相关部分:

  

<强> §17.4.4 Synchronization Order

     

[..]写入易失性变量 v (§8.3.1.4) v 的所有后续读取同步 - 任何线程(根据同步顺序定义“后续”)。 [..]

具体机器的内存模型并不重要,因为Java编程语言的语义是用抽象机器定义的 - 独立于具体机器。 Java运行时环境的责任是以这样的方式执行代码,它符合 Java语言规范提供的保证。


关于实际问题:

  • 如果没有进一步的同步,方法read2可以打印"Bar",因为read2可以在write之前执行。
  • 如果与CountDownLatch进行了额外的同步,以确保在 read2之后执行了write,那么方法read2将永远不会打印"Bar",因为与CountDownLatch的同步会删除x上的数据竞赛

独立的易变量:

对volatile变量的写入是否与读取任何其他volatile变量同步是否有意义?

是的,这很有道理。如果两个线程需要相互交互,则它们通常必须使用相同的volatile变量才能交换信息。另一方面,如果一个线程使用volatile变量而不需要与所有其他线程进行交互,我们不想为内存障碍支付费用。

实际上这很重要。让我们举个例子。以下类使用volatile成员变量:

class Int {
    public volatile int value;
    public Int(int value) { this.value = value; }
}

想象一下,这个类只在方法中本地使用。 JIT编译器可以轻松检测到该对象仅在此方法中使用(Escape analysis)。

public int deepThought() {
    return new Int(42).value;
}

根据上述规则,JIT编译器可以删除volatile读写的所有效果,因为volatile变量无法从任何其他线程访问。

这种优化实际上存在于Java JIT编译器中:

答案 1 :(得分:1)

据我所知,问题实际上是关于易失性读/写及其发生前保证。说到那一部分,我只有一件事要添加到nosid的回答中:

在正常写入之前无法移动易失性写入,正常读取后无法移动易失性读取。这就是为什么read1()read2()结果会像nosid写的那样。

谈到障碍 - 定义对我来说听起来不错,但可能让你感到困惑的一件事就是这些是事物/工具/方式/机制(称之为你喜欢的任何方式)来实现热点中JMM中描述的行为。使用Java时,您应该依赖JMM保证,而不是实现细节。