Java中的内存屏障行为

ast*_*icx 32 java multithreading java-memory-model memory-barriers

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

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

  1. 线程A在写入易失性字段f时可见的任何内容在读取f时都会对线程B可见.
  2. 请注意,两个线程都必须访问相同的volatile变量才能正确设置before-before关系.情况并非如此,线程A在写入易失性字段f时可见的所有内容在读取易失性字段g后变为线程B可见.

但是当我查看另一篇关于内存障碍的博客时,我得到了这些:

  1. 商店屏障,x86上的"sfence"指令强制屏障之前的所有存储指令发生在屏障之前,并将存储缓冲区刷新到缓存以供发布它的CPU.
  2. 负载屏障,在x86"lfence"指令,强制所有加载指令之后的阻隔后发生障碍,然后等待负载缓冲区对于CPU流失.

对我来说,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");
      }
    }
  }
Run Code Online (Sandbox Code Playgroud)

比方说,我们有1个写线程TW1首先调用内存屏障的write()方法,那么我们有2个读线程TR1和TR2调用内存屏障的READ1()和读取2()method.Consider在CPU不保留订购此程序运行(86 DO保存订货为这样的情况下,其不是这种情况),根据存储器模型,将有一个StoreStore阻挡(比方说SB1)W01/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".但如果内存屏障像博客中描述的Martin Thompson那样运行,Store屏障会将所有数据推送到主内存,Load barrier会将所有数据从主内存提取到缓存/缓冲区,然后TR2也会保证在x上看到14.

我不确定哪一个是正确的,或者两者都是正确的,但Martin Thompson所描述的仅适用于x86架构.JMM不保证更改为x对TR2可见,但x86实现确实如此.

谢谢〜

nos*_*sid 16

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

§17.4.4同步顺序

[..]对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中"后续"根据同步顺序定义).[..]

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


关于实际问题:

  • 如果没有进一步的同步,则该方法read2可以打印"Bar",因为read2之前可以执行write.
  • 如果存在与a的额外同步以CountDownLatch确保在之后read2执行,则方法将永远不会打印,因为同步将删除数据争用. writeread2"Bar"CountDownLatchx

独立的易变量:

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

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

这在实践中确实很重要.让我们举个例子.以下类使用volatile成员变量:

class Int {
    public volatile int value;
    public Int(int value) { this.value = value; }
}
Run Code Online (Sandbox Code Playgroud)

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

public int deepThought() {
    return new Int(42).value;
}
Run Code Online (Sandbox Code Playgroud)

根据上述规则,JIT编译器可以删除volatile读写的所有影响,因为该volatile变量不能从任何其他线程访问.

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