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

以下是道格·利阿(Doug Lea)在其有关JMM的澄清文章中的两句话,两者都很简单:

  • 线程A写入 volatile 字段f时对线程A可见的所有内容在线程b读取f时对线程B可见。
  • 请注意,两个线程访问相同的volatile变量很重要,以便正确设置事前发生的关系。并非所有情况下,线程A写入 volatile 字段f时对线程A可见的所有内容在线程B读取 volatile 字段g之后变为对线程B可见。

  • 但是当我查看关于内存障碍的另一个blog时,我得到了这些:
  • 存储屏障,即x86上的“防护”指令,强制所有在屏障之前的存储指令都在屏障之前发生,并刷新存储缓冲区以缓存发布它的CPU。
  • x86上的“fence”指令是一个装入屏障,它会在屏障之后强制执行所有装入指令,并在屏障之后执行,然后等待装入缓冲区为该CPU耗尽。

  • 对我来说,Doug Lea的澄清比另一澄清更严格:基本上,这意味着如果负载障碍和存储障碍在不同的监视器上,则不能保证数据的一致性。但是后一种方法意味着即使障碍位于不同的监视器上,也可以保证数据的一致性。我不确定我是否正确理解了这2个,也不确定其中哪个是正确的。

    考虑以下代码:
      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()方法。请考虑该程序在不保留排序(x86)的CPU上运行对于这种情况,请务必保留排序(不是这种情况),根据内存模型,W01/W02之间将存在StoreStore屏障(假设为SB1),R11/R12和R21/R22之间将存在2个LoadLoad屏障(让我们例如RB1和RB2)。
  • 因为SB1和RB1在同一台监视器i上,所以调用read1的线程TR1应该始终在x上看到14,因此也始终打印“Foo”。
  • SB1和RB2在不同的监视器上,如果Doug Lea是正确的,则不能保证线程TR2在x上看到14,这意味着“Bar”可能会偶尔打印出来。但是,如果内存屏障的运行方式类似于blog中描述的Martin Thompson,则存储屏障会将所有数据推送到主内存,而负载屏障会将所有数据从主内存拉到缓存/缓冲区,那么TR2也将保证在x上看到14。

  • 我不确定哪一个是正确的,或者两者都正确,但是Martin Thompson描述的只是x86体系结构。 JMM不保证TR2可以看到对x的更改,但是x86实现可以。

    谢谢〜

    最佳答案

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



    具体机器的内存模型无关紧要,因为Java编程语言的语义是根据抽象机器定义的-与具体机器无关。 Java运行时环境有责任以这种方式执行代码,使其符合Java语言规范所提供的保证。

    关于实际问题:

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


  • 独立易失变量:

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

    是的,这很有道理。如果两个线程需要相互交互,则它们通常必须使用相同的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;
    }
    

    使用上述规则,由于无法从任何其他线程访问volatile变量,因此JIT编译器可以删除volatile读取和写入的所有影响。

    这种优化实际上存在于Java JIT编译器中:
  • src/share/vm/opto/memnode.cpp
  • 关于java - Java中的内存屏障行为,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/24469063/

    10-09 01:49