【问题标题】:Reading a stale value after a newer value was read [duplicate]在读取新值后读取陈旧值[重复]
【发布时间】:2020-11-24 09:25:56
【问题描述】:

考虑这个例子。我们有:

int var = 0;

线程 A:

System.out.println(var);
System.out.println(var);

线程 B:

var = 1;

线程同时运行。下面的输出可能吗?

1
0

即读取新值后读取原始值。 var 不是易变的。我的直觉是这是不可能的。

【问题讨论】:

  • 不同步地从不同线程读写变量是错误的。
  • @AlexeiKaigorodov 假设我不在乎读取过时的值,即使是很长一段时间。我只关心在同一个线程中读取一个新值,然后再次读取旧值。
  • 请问这个问题背后的动机是什么?在我看来,好像对某事有些疑惑,但你还没有真正解释清楚。
  • 删除用例是不好的举动,因为用例与您的问题不同。随后的两个System.out.println(var); 语句无法感知新值之后的旧值,因为println 在内部使用synchronized,具有比必要更广泛的副作用。这是一个实施细节,但现在已经存在了四分之一个世纪。
  • 你的用例太棒了,恕我直言;并且会触及 JLS 中迄今为止并非微不足道的两点:program orderhappens beforehappens before consistency,后者将证明 return var 可以返回零,即使您输入了 if 块.

标签: java concurrency java-memory-model


【解决方案1】:

您正在使用System.out.println,它在内部执行synchronized(this) {...},这会使事情变得更糟。但即便如此,您的读者线程仍然可以观察到1, 0,即:一个活泼的阅读。

到目前为止,我还不是这方面的专家,但是在浏览了 Alexey Shipilev 的大量视频/示例/博客之后,我想我至少了解了一些东西。

JLS states那个:

如果 x 和 y 是同一线程的操作,并且 x 在程序顺序中位于 y 之前,则为 hb(x, y)。

由于var的两个读取都在program order中,我们可以绘制:

                (po) 
firstRead(var) ------> secondRead(var)
// po == program order

那句话还说,这建立了happens-before 订单,所以:

                (hb) 
firstRead(var) ------> secondRead(var)
// hb == happens before

但那是在“同一个线程”中。如果我们想推理多线程,我们需要查看synchronization order。我们需要它,因为关于 happens-before order 的同一段说:

如果动作 x 后续动作 y 同步,那么我们也有 hb(x, y)。

因此,如果我们在program ordersynchronizes-with order 之间构建这个动作链,我们就可以推断结果。让我们将其应用于您的代码:

            (NO SW)                    (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)

// NO SW == there is "no synchronizes-with order" here
// hb    == happens-before

这就是happens-before consistencysame chapter 中发挥作用的地方:

如果对于 A 中的所有读取 r,一组动作 A 在发生之前是一致的,其中 W(r) 是 r 看到的写入动作,但不是 hb(r, W(r))或者在 A 中存在写 w 使得 wv = rv 和 hb(W(r), w) 和 hb(w, r)。

在happens-before一致的一组动作中,每次读取都会看到一次写入,而happens-before排序则允许它看到

我承认我对第一句话的理解非常模糊,正如他所说,这是 Alexey 对我帮助最大的地方:

读取查看happens-before 中发生的最后一次写入或任何其他写入

因为那里没有synchronizes-with order,并且隐含地没有happens-before order,所以允许读取线程通过竞赛读取。 从而得到1,而不是0


只要你介绍一个正确的synchronizes-with orderfor example one from here

监视器 m 上的解锁操作与...上的所有后续锁定操作同步

对 volatile 变量 v 的写入与任何线程对 v 的所有后续读取同步...

图表发生变化(假设您选择制作varvolatile):

               SW                       PO
write(var) ---------> firstRead(var) -------> secondRead(var)

// SW == there IS "synchronizes-with order" here
// PO == happens-before

PO(程序顺序)通过我在 JLS 的这个答案中引用的第一句话给出了 HB(发生在之前)。而SW 给出HB 因为:

如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。

这样:

               HB                       HB
write(var) ---------> firstRead(var) -------> secondRead(var)

现在happens-before order表示读取线程将读取“写入最后一个HB”的值,或者这意味着读取1然后读取0是不可能的。


我以jcstress samples 为例,做了一个小改动(就像你的System.out.println 所做的那样):

@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    private static class Holder {

        int a;
        int trap;
    }

    @Actor
    public void actor1() {
        h1.a = 1;
    }

    @Actor
    public void actor2(II_Result r) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;
        
        h1.trap = 0;
        h2.trap = 0;

        synchronized (this) {
            r.r1 = h1.a;
        }

        synchronized (this) {
            r.r2 = h2.a;
        }

    }

}

注意synchronized(this){....} 不是初始示例的一部分。即使有同步,我仍然可以看到 1, 0 结果。这只是为了证明即使使用synchronized(内部来自System.out.println),您仍然可以获得1 而不是0

【讨论】:

  • 所以你是说,没有println()1 0 1可能会发生?
  • @FrancescoMenzani 理论上,它甚至可能发生在println()
【解决方案2】:

当读取var 的值并且它是1 时,它不会变回。由于可见性或重新排序,此输出不会发生。可能发生的情况是0 00 11 1

这里要理解的重点是println涉及同步。查看该方法的内部,您应该会在那里看到synchronized。这些块具有打印将按该顺序发生的效果。虽然写入可以随时发生,但第一次打印看到 var 的新值是不可能的,但第二次打印看到旧值是不可能的。因此,写入只能发生在两次打印之前、中间或之后。

除此之外,无法保证写入完全可见,因为var 没有标记为volatile,写入也不会以任何方式同步。

【讨论】:

  • @Holger 这是基于这样一个事实,即 A 对 var 进行了两次打印/读取,B 对var 进行了一次写入/读取。假设写入对 A 可见,它会发生在打印之前,中间或之后,这导致了我提到的可能性。如果写不可见,那么它只是0 0。如果有重新排序,而println 中没有synchronized,那么仍然没有事件序列化会导致A 在读取0 后神奇地打印0。我错过了什么?
  • 首先,答案应该包括推理。并且“假设写入对 A 可见”没有抓住重点,因为问题特别是关于不可见性,因为变量不是 volatile。因为在这种情况下,没有保证,读取可能碰巧感知到更新的值,但这不会追溯建立之前发生的关系。进一步的“事件序列化”不是一回事。只有println中的synchronized会影响这个具体的例子,所以在回答中不提是不负责任的。
  • 1 0 不可能发生,只要 println 的内部阻止它。但一般来说,如果没有额外的同步原语,读取可能被不同线程修改的非易失性变量是一种激烈的读取。并且对于两个后续的快速读取,没有可见性保证,并且看起来后续读取的内容可能会感知到比前一次读取更旧的值。你没有说任何关于发生前发生的事情,但你应该,因为只有the specification定义的那些关系才重要。
  • 在对该主题和类似示例进行更多研究时,我实际上找到了完全相同的副本:Reordering of reads。如果println 中没有同步,1 0 将成为可能,这让我大吃一惊。再次感谢您指出我的误解,@Holger。
【解决方案3】:

我认为这里缺少的是这些线程在实际物理内核上运行的事实,而我们这里几乎没有可能的变体:

  1. 所有线程都在同一个内核上运行,那么问题就归结为这 3 条指令的执行顺序,在这种情况下 1,0 是不可能的,因此不包括 1,0

  2. A 和 B 在 2 个不同的核心上运行,那么 1,0 看起来也不可能,只要运行线程 A 的核心读取 1,它就不可能在之后读取 0,与上面的 printlns 相同.

  3. 线程 A 在这 2 个 println 之间重新调度,因此第二个 println 在不同的核心上执行,要么与 B 相同/将被执行,要么在不同的第三个核心上执行。因此,当 2 个 printlns 在不同的核心上执行时,这取决于 2 个核心看到什么值,如果 var 不同步(不清楚 var 是否是其中的成员),那么这 2 个核心可以看到不同的 var 值,所以有 1,0 的可能性。

所以这是一个缓存一致性问题。

附:我不是 jvm 专家,所以这里可能还有其他问题。

【讨论】:

  • 您忽略的是 JIT 编译器/优化器,它可能会将代码转换为与您在源代码中编写的内容完全不同的东西。当您不知道实际代码的样子时,讨论代码如何受到 CPU 架构的影响是没有意义的。缓存一致性是最小的问题。
  • JIT 可以为所欲为,除了破坏内存模型。
  • 现在,您已经接近正确答案了。 The memory model 指定什么是合法执行,而不是您写的关于“实际物理内核”或线程调度的内容。
  • 如前所述,如果您不了解 JIT 可以做什么或不可以做什么,那么讨论 JIT 的结果将如何与物理架构交互是没有意义的。
  • JLS 是唯一相关的事情,因为当硬件架构允许 JLS 禁止的某些事情时,JVM 开发人员有责任防止这种情况发生。例如,JLS 禁止投机执行将条件变成自我实现的预言(“凭空出现”值)。因此,对于具有这种推测执行(Alpha AFAIK)的架构,JVM 开发人员必须限制其影响以符合 JLS。因此,最终,对于 Java 开发人员来说,行为是由 JIT 还是由硬件引起的并不重要,因为它只是因为 JLS 允许而发生。
【解决方案4】:

添加到其他答案:

对于longdouble,写入可能不是原子的,因此前 32 位可能在最后 32 位之前变得可见,反之亦然。因此可以输出完全不同的值。

【讨论】:

    猜你喜欢
    • 2018-11-12
    • 1970-01-01
    • 2012-12-17
    • 2018-08-27
    • 1970-01-01
    • 2013-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多