【问题标题】:Does StoreStore memory barrier in Java forbid the read-write reordering?Java中的StoreStore内存屏障是否禁止读写重新排序?
【发布时间】:2020-01-30 18:40:37
【问题描述】:

现在我们有

Load A
StoreStore
Store B

有没有可能实际的执行顺序如下

StoreStore
Store B
Load A

如果可能,如何解释似乎违反The Java volatile Happens-Before Guarantee 的情况。

据我所知,volatile语义是使用下面的JMM内存屏障添加策略实现的

insert a StoreStore before volatile variable write operation
insert a StoreLoad after volatile variable write operation
insert a LoadLoad after volatile variable read operation
insert a LoadStore after volatile variable read operation

现在如果我们有两个java线程如下

线程 1

Load A
StoreStore
Store volatile B

线程 2

Load volatile B
Load C

根据“Java volatile Happens-Before Guarantee”,当Load volatile B 在Store volatile B 之后时,Load A 应该在Load C 之前发生,但如果Load A 可以重新排序为“在 Store volatile B 之后” ,Load A is before Load C如何保证?

【问题讨论】:

  • "在 volatile 变量写入操作后插入 StoreLoad" - 不,它插入 LoadStore before volatile 写入操作。因此,在您的示例中,Load A 无法使用以下 Store volatile B 重新排序。

标签: java memory-barriers instruction-reordering


【解决方案1】:

从技术上讲,Java 语言没有内存屏障。相反,Java 内存模型是根据 happens before 关系指定的;详情请看以下内容:

您讨论的术语来自The JSR-133 Cookbook for Compiler Writers。正如该文档所说,它是编写实现 Java 内存模型的编译器的人的指南。它解释了 JMM 的含义,显然不打算成为官方规范。 JLS 是规范。

JSR-133 Cookbook 中关于内存屏障的部分根据它们限制特定加载和存储序列的方式对它们进行分类。对于StoreStore 障碍,它说:

序列:Store1; StoreStore; Store2 确保 Store1 的数据在与 Store2 关联的数据和所有后续存储指令之前对其他处理器可见(即刷新到内存)。一般来说,处理器需要StoreStore屏障,否则不能保证从写入缓冲区和/或缓存到其他处理器或主内存的刷新的严格顺序。

如您所见,StoreStore 屏障仅限制 store 操作的行为。

在您的示例中,您有一个load,后跟一个store。 StoreStore 屏障的语义没有说明 load 操作。因此,您建议的重新排序是允许的。

【讨论】:

  • 感谢您的回复。我已经更改了我的问题,但仍然让我感到困惑。
  • 能再帮我一把吗?
  • 您对@Tsyvarev 评论的回答解决了我的问题。
【解决方案2】:

这只是回答您问题的更新部分。

首先,您提供的示例不是 Java 代码。因此我们不能对它应用 JMM 推理。 (只是为了让我们清楚这一点。)

如果您想了解 Java 代码的行为方式,忘记内存屏障。 Java 内存模型告诉您为使内存读取和写入具有保证行为所需的一切。为了推理(正确的)行为,你需要知道的一切。所以:

  • 编写您的 Java 代码
  • 分析代码以确保在线程需要读取另一个线程写入的值的所有情况下都有适当的发生在链。
  • 将您的(正确的)Java 代码编译为机器指令的问题留给编译器。

查看您示例中的伪指令序列,它们没有多大意义。我不认为真正的 Java 编译器会(在内部)在编译真正的 Java 代码时使用这样的障碍。相反,我认为在每次 volatile 写入之后和每次 volatile 读取之前都会有一个 StoreLoad 内存屏障。

让我们考虑一些真实的 Java 代码 sn-ps:

public int a;
public volatile int b;

// thread "one"
{
  a = 1;
  b = 2;
}

// thread "two"
{ 
  if (b == 2) {
      print(a);
  }
}

现在假设线程“two”中的代码在线程“one”之后执行,将会有这样一个happens-before链:

  • a = 1 发生之前b = 2
  • b = 2 发生在b == 2
  • b == 2 发生之前print(a)

除非涉及一些其他代码,否则happens-before链意味着线程“二”将打印“1”。

注意:

  1. 编译代码时无需考虑编译器使用的内存屏障。
  2. 障碍是特定于实现的并且是编译器内部的。
  3. 如果您查看本机代码,您将不会看到内存屏障本身。您将看到具有所需语义的本机指令,以确保存在(隐藏的)内存屏障。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-18
    • 2010-12-30
    • 2010-12-07
    • 1970-01-01
    • 2013-08-31
    • 1970-01-01
    相关资源
    最近更新 更多