Java中内存屏障的行为

Posted

技术标签:

【中文标题】Java中内存屏障的行为【英文标题】:Behavior of memory barrier in Java 【发布时间】:2014-08-19 13:57:09 【问题描述】:

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

以下是 Doug Lea 在他的一篇关于 JMM 的澄清文章中引用的两句话,这两句话都非常直截了当:

    线程 A 在写入 volatile 字段 f 时可见的任何内容在线程 B 读取 f 时变为可见。 请注意,两个线程访问同一个 volatile 变量以正确设置发生前的关系非常重要。并不是线程 A 在写入 volatile 字段 f 时对它可见的所有内容在它读取 volatile 字段 g 后对线程 B 都是可见的。

但是当我查看另一个关于内存屏障的 blog 时,我得到了这些:

    存储屏障,x86 上的“sfence”指令,强制屏障之前的所有存储指令发生在屏障之前,并将存储缓冲区刷新到发出它的 CPU 的缓存中。 加载屏障,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");
      
    
  

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

    由于 SB1 和 RB1 在同一个监视器 i 上,所以调用 read1 的线程 TR1 应该总是在 x 上看到 14,而且总是打印“Foo”。李> SB1 和 RB2 在不同的显示器上,如果 Doug Lea 正确,线程 TR2 将不能保证在 x 上看到 14,这意味着可能偶尔会打印“Bar”。但是如果内存屏障像blog 中描述的 Martin Thompson 那样运行,Store 屏障会将所有数据推送到主内存,而负载屏障会将所有数据从主内存拉到缓存/缓冲区,那么 TR2 也将保证看到 14 on x.

我不确定哪一个是正确的,或者两者都是正确的,但 Martin Thompson 所描述的只是针对 x86 架构。 JMM 不保证对 x 的更改对 TR2 可见,但 x86 实现可以。

谢谢~

【问题讨论】:

您不应该关心 x86 上的内存屏障。 Java 的语义和Java 内存模型是在抽象机器 上定义的。这是唯一重要的事情。 Java 运行时负责确保 抽象机器 做出的保证在运行时得到满足。 事实上,x86 语义(包括缓存一致性)比 jmm 要求的要强,但是如果您不从事正如 nosid 正确指出的那样,用于 x86 的 java 运行时。 您的担忧是有效的。 /可能/读者 2 会打印 Bar。但是,除非读取器线程之前已与内存屏障类交互并缓存了 x 的值,否则读取器 2 将打印 foo,因为它将第一次访问 x。与 write 的交互意味着对 x 的更改将是可见的。也许更有趣的测试是让 read1 和 read2 在 W1 之前和之后执行。 CountDownLatch 引入了额外的同步。因此,如果您使用CountDownLatch 确保read2write 之后执行,那么read2 将始终打印"Foo" @asticx:答案是:"Bar" 显然是可能的,如果writeread2 之间没有同步,则不可能,如果有同步。我想这不是你感兴趣的。所以,请提供更多你真正想知道的信息。 【参考方案1】:

道格·李是对的。您可以在Java 语言规范的§17.4.4 部分找到相关部分:

§17.4.4 Synchronization Order

[..] 对 volatile 变量 v 的写入(第 8.3.1.4 节)同步所有后续对 v 的读取任何线程(其中“后续”是根据同步顺序定义的)。 [..]

具体机器的内存模型无关紧要,因为Java编程语言的语义是根据抽象机器定义的——独立于混凝土机械。 Java 运行时环境有责任以这样的方式执行代码,使其符合Java 语言规范给出的保证。


关于实际问题:

如果没有进一步同步,方法read2可以打印"Bar",因为read2可以在write之前执行。 如果有一个与CountDownLatch 的额外同步以确保read2write 之后执行,那么方法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 编译器中:

src/share/vm/opto/memnode.cpp

【讨论】:

澄清一下,如果write发生read2,按时间顺序没有额​​外同步,"Bar"还能打印吗? @SotiriosDelimanolis 我认为不,14 只能从x 读取之后(我的意思是,发生在之后在write 中,所以只会打印Foo @AlexeyMalev 是的,但是在此之前,它读作j,而不是i,所以IMO 引用1. 不适用。 @SotiriosDelimanolis:如果read2 发生在write 之后,你怎么知道(无需额外同步)? @Voo 我猜 x86 提供了比 JMM 更强的保证,以使 read2 线程可以看到对 x 的更改。 JMM 本身并不保证会发生这种情况。【参考方案2】:

据我了解,问题实际上是关于易失性读/写及其发生前的保证。说到这部分,我对 nosid 的回答只有一件事要补充:

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

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

【讨论】:

以上是关于Java中内存屏障的行为的主要内容,如果未能解决你的问题,请参考以下文章

解密内存屏障

解密内存屏障

内存屏障后和互锁操作后内存缓存一致性的时序

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

Java多线程内存读写 —— 内存屏障的理解