【问题标题】:Java volatile reordering prevention scopeJava volatile 重新排序预防范围
【发布时间】:2020-01-04 23:01:40
【问题描述】:

对 volatile 字段的写入和读取可防止在 volatile 字段之前和之后的读取/写入分别重新排序。对 volatile 变量的写入之前的变量读/写不能重新排序以发生在它之后,并且从 volatile 变量读取之后的读/写不能重新排序以发生在它之前。但是这个禁令的范围是什么?据我了解 volatile 变量只能在使用它的块内阻止重新排序,对吗?

为了清楚起见,让我举一个具体的例子。假设我们有这样的代码:

int i,j,k;
volatile int l;
boolean flag = true;

void someMethod() {
    int i = 1;
    if (flag) {
        j = 2;
    }
    if (flag) {
        k = 3;
        l = 4;
    }
}

显然,写入l 将阻止写入k 重新排序,但它会阻止写入ij 相对于l 重新排序?换句话说,写入ij 可以在写入l 之后发生吗?

更新 1

感谢大家抽出宝贵时间回答我的问题 - 我很感激。问题是你回答错了问题。我的问题是关于范围,而不是关于基本概念。问题基本上是在代码中,编译器能保证与 volatile 字段的“发生在之前”的关系。 显然编译器可以保证在同一个代码块内,但是封闭块和对等块呢 - 这就是我的问题所在。 @Stephen C 说,不稳定的保证发生在整个方法体内的行为之前,即使在封闭块中,但我找不到任何确认。他说的对吗,有什么地方可以确认吗?

让我再举一个关于范围界定的具体例子来澄清事情:

setVolatile() {
   l = 5;
} 

callTheSet() {
   i = 6;
   setVolatile();
}

在这种情况下,编译器会禁止i write 的重新排序吗?或者也许编译器不能/没有被编程来跟踪在 volatile 的情况下在其他方法中发生的情况,并且i write 可以重新排序以在setVolatile() 之前发生?或者编译器根本不会重新排序方法调用?

我的意思是在某个地方必须有一个点,当编译器将无法跟踪某些代码是否应该在某些易失性字段写入之前发生。否则,一个易失性字段写入/读取可能会影响一半程序的排序,如果不是更多的话。这是一种罕见的情况,但有可能。

另外,看看这句话

在新的内存模型下,volatile 变量之间不能相互重新排序仍然是事实。不同之处在于,现在对围绕它们的正常字段访问重新排序不再那么容易了。

“围绕着他们”。这句话暗示,有一个 volatile 字段可以防止重新排序的范围。

【问题讨论】:

  • 关于什么重新排序?彼此?写信给l?还有什么?
  • 当然是关于“l”。我认为这很清楚,但我会说得更清楚。谢谢。
  • @NikKotovski 你是在问ij 是否可以在写到l 下方重新排序?
  • 我喜欢认为这是一个障碍和可以向下/向上跳跃(重新排序)的操作(按顺序或阅读代码)。这可能有助于stackoverflow.com/questions/45151763/…。但要回答您的问题,这些是独立阅读,高于l(障碍)任何东西都可以重新排序。关键是其他一些线程似乎是l 的写(观察它),他们也会观察写它ij - 因此你并不真正关心这个内部重新排序(如果它发生了)
  • @JohnVint 是的。由于它们在其他块中,i 甚至在封闭块中,它们可以重新排序以在l 之后发生吗?

标签: java multithreading concurrency volatile


【解决方案1】:

显然,写入 l 会阻止写入 k 重新排序,但会阻止写入 i 和 j 的重新排序吗?

您所说的重新排序是什么意思并不完全清楚;见上面我的 cmets。

但是,在 Java 5+ 内存模型中,我们可以说在写入 l 之前发生的对 ij 的写入将在读取 l 之后对另一个线程可见。 .. 前提是在写入l 之后没有写入ij

这确实会限制写入ij 的指令的任何重新排序。具体来说,在写入l 之后,它们不能移动到内存写入屏障之后,因为这可能导致它们对第二个线程不可见。

但是这个禁令的范围是什么?

没有禁令本身

您需要了解指令、重新排序和内存屏障只是实现 Java 内存模型的特定方式的细节。该模型实际上是根据在任何“格式良好的执行”中保证可见的内容来定义的。

据我了解,volatile 会阻止在使用它的块内重新排序,对吗?

实际上,没有。块不考虑在内。重要的是方法中语句的(程序源代码)顺序。


@Stephen C 说,不稳定的保证发生在整个方法体内的行为之前,即使在封闭块中,但我找不到任何确认。

确认是 JLS 17.4.3。它声明如下:

在每个线程 t 执行的所有线程间动作中,t 的程序顺序是一个总顺序,它反映了根据 t 的线程内语义执行这些动作的顺序。

如果所有动作都以与程序顺序一致的总顺序(执行顺序)发生,则一组动作是顺序一致的,并且变量v的每次读取r看到写入w写入v的值这样:

  • w 在执行顺序中位于 r 之前,并且

  • 在执行顺序中,没有其他写入 w' 使得 w 在 w' 之前并且 w' 在 r 之前。

顺序一致性是对程序执行中的可见性和顺序的非常强大的保证。在顺序一致的执行中,所有单个操作(例如读取和写入)都有一个与程序顺序一致的总顺序,并且每个单独的操作都是原子的,并且对每个线程都是立即可见的。

如果一个程序没有数据竞争,那么该程序的所有执行将看起来是顺序一致的。

请注意,此定义中没有提及块或范围。

【讨论】:

  • Actually, no. The blocks don't come into the consideration. What matters is the (program source code) order of the statements within the method. 那么范围就是方法的代码块。另外请问你能找到证据吗?我并不是要装腔作势,只是不要相信没有证据的答案。人们在这里给出了很多错误的答案。如果我自己能找到证据,我当然不会来这里。
  • 证明是 JLS。特别是第 17.4.3 章到 17.4.5 章。如果您真的非常想了解 JMM,那就是指定它的地方。我可以使用 JMM 术语为您的示例构建一个特定的证明......但您最好自己阅读它。 docs.oracle.com/javase/specs/jls/se8/html/jls-17.html。当你这样做时,你会发现“块”和“范围”的概念确实被考虑在内了。
  • 对不起...我的意思是他们不考虑在内。
【解决方案2】:

编辑 2

volatile 仅供参考the happens-before relation

为什么它在单线程中重新排序

考虑到我们有两个字段:

int i = 0;
int j = 0;

我们有办法写出来

void write() {
  i = 1;
  j = 2;
}

如您所知,编译器可能会重新排序它们。那是因为编译器认为先访问哪个并不重要。因为在单线程中,它们“一起发生”。

为什么不能在多线程中重新排序

但现在我们有另一种方法可以在另一个线程中读取它们:

void read() {
  if(j==2) {
    assert i==1;
  }
}

如果编译器仍然对其进行重新排序,则此断言可能会失败。这意味着j 一直是2,但i 却不是1。似乎i=1 发生在assert i==1 之后。

volatile 做什么

volatile 唯一保证the happens-before relation

现在我们添加volatile

volatile int j = 0;

当我们观察到j==2 为真时,这意味着j=2 已经发生并且i=2 在它之前,它必须发生。所以现在断言永远不会失败。

“防止重新排序”只是编译器提供该保证的一种方法。

结论

您现在唯一应该做的就是happens-before。请参考下面的java规范链接。 reordering or not 只是此保证的side effect

回答你的问题

由于lvolatile,因此在访问someMethod 中的l 之前始终访问ij。事实上,l=4 行之前的每一件事都会发生在它之前。

编辑 1

由于帖子已被编辑。这是进一步的解释。

对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

happens-before 表示:

如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。

所以访问ij 发生在访问l 之前。

参考:https://docs.oracle.com/javase/specs/jls/se10/html/jls-17.html#jls-17.4.5

原答案

不,volatile 仅保护自己,尽管在volatile 附近重新排序字段访问并不容易。

在新的内存模型下,volatile 变量之间不能相互重新排序仍然是事实。 不同之处在于,现在对围绕它们的正常字段访问重新排序不再那么容易了。 写入 volatile 字段与释放监视器具有相同的记忆效果,而从 volatile 字段中读取具有相同的记忆效果作为监视器获得的记忆效应。实际上,由于新的内存模型对 volatile 字段访问与其他字段访问(无论是否为 volatile)的重新排序设置了更严格的限制,因此线程 A 在写入 volatile 字段 f 时可见的任何内容在线程 B 读取 f 时变得可见。

volatile 关键字仅保证:

对 volatile 字段的写入发生在对同一 volatile 的每次后续读取之前。

参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile

【讨论】:

  • Reads from and writes to other variables cannot be reordered to occur after a write to a volatile variable, if the reads / writes originally occurred before the write to the volatile variable. The reads / writes before a write to a volatile variable are guaranteed to "happen before" the write to the volatile variable. Source. 另外请看一下@Stephen C 的回答以及我们与他的讨论。
  • @NikKotovski 参考是对的。但是你的问题是will it prevent reordering of writes to i and j?,答案是no
  • (@NikKotovski - 我注意到您还没有纠正您问题中的歧义。Dean 将您的问题解释为意味着重新排序写入 i 相对于写入 j。这实际上是明显的解释对我也是如此。)
  • 虽然常见问题解答是正确的,但它不是确定的。 JLS 是权威的,也更加严格。
  • @NikKotovski 为什么你认为它们不相关?我告诉过volatile 只有the happen-before relation。我稍后会更新我的答案,给你一个样本。这将是我最后一次修改,如果您有任何问题,请参考 java 规范。
【解决方案3】:

我很想知道 volatile 变量如何影响其他字段

易变变量确实会影响其他字段。如果 JIT 编译器认为重新排序不会对执行输出产生任何影响,他可以重新排序指令。因此,如果您有 6 个独立变量存储,JIT 可以重新排序指令。

但是,如果您将变量设置为 volatile,即在您的情况下使用变量 l,则 JIT 不会在 volatile STORE 之后重新排序任何变量 STORES。而且我认为这是有道理的,因为在多线程程序中,如果我将变量 l 的值设为 4,那么我应该将 i 设为 1,因为在我的程序中 i 是在 l 之前写的,最终是 Program Order Semantics(如果我没记错的话)。

Note that volatile variables does two things:
  1. 编译器不会在 volatile 存储之后对任何存储重新排序/在 volatile 读取之前不会对任何读取重新排序。
  2. 刷新加载/存储缓冲区,以便所有处理器都可以看到更改。

编辑:

这里的好博客:http://jpbempel.blogspot.com/2013/05/volatile-and-memory-barriers.html

【讨论】:

  • Volatile variables doesn't affect the other fields 所以你说 volatile 关键字可以防止对其他字段的操作在 volatile 写入后重新排序,但你说 volatile 不会影响它们(其他字段)。你觉得这里的一切都合适吗?
【解决方案4】:

也许我知道你的“真实范围”。

两种类型的重排序是导致指令结果无序的主要原因: 1.编译器优化 2.cpu处理器记录(可能是缓存和主存同步造成的)

volatile关键字首先需要确认volatile变量的flush,同时其他变量也被flush到主存中。但是由于编译器的重新排序,volatile valatile变量之前的一些可写指令可能会在volatile变量之后重新排序,读者可能会混淆读取程序顺序中 volatile 变量之前的其他变量值不是实时的,因此制定了“变量写指令在 volatile 变量之前被强制运行在 volatile 之前”的规则。这个优化是由 Java 编译器或 JIT 完成。

重点是编译器在指令中的优化,比如查找死代码、指令重排序操作,指令代码范围始终是一个“基本块”(除了一些其他的常量传播优化等)。 基本块是内部没有jmp指令的一组指令,所以这是一个基本块。所以在我看来,重新排序操作是固定在范围基本块中的。 源代码中的基本块始终是块或方法体。

而且由于java没有内联函数,方法调用是由动态invoke方法指令使用的,reorder操作不应该跨两个方法。

所以,范围不会大于“方法体”,或者可能只是“for”体的一个区域,这是基本的块范围。

这是我的全部想法,我不确定它是否正确,有人可以帮助使其更准确。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-06-02
    • 1970-01-01
    • 2013-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-19
    相关资源
    最近更新 更多