【问题标题】:Java Memory Model: a JLS statement about sequential consistency seems incorrectJava 内存模型:关于顺序一致性的 JLS 声明似乎不正确
【发布时间】:2021-01-01 04:12:20
【问题描述】:

我正在阅读Chapter 17. Threads and Locks of JLS,以下关于 Java 中顺序一致性的陈述对我来说似乎不正确:

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

他们将数据竞赛定义为:

当一个程序包含两个冲突的访问(第 17.4.1 节)时,这些访问没有按发生前的关系排序,则称为包含数据竞争。

他们将冲突访问定义为:

如果至少有一次访问是写入,则对同一变量的两次访问(读取或写入)称为冲突。

最后他们有以下关于发生前的关系:

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

我对第一条语句的问题是我认为我可以想出一个没有数据竞争并且允许顺序不一致执行的 Java 程序:

// Shared code
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Thread1       Thread2
   v1 = 1;
   v2 = 2;
   vv = 10;      while(vv == 0) {;}
                 int r1 = v1;
                 int r2 = v2;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

在上面的代码中,我还用缩进方式展示了线程的代码是如何在运行时交错的。

所以,据我了解,这个程序:

  • 没有数据争用:Thread2 中 v1 和 v2 的读取与 Thread1 中的写入同步
  • 可以输出v1=1 v2=4(这违反了顺序一致性)。

因此,JLS 的初始声明

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

对我来说似乎不正确。

是我遗漏了什么还是我在某处犯了错误?

编辑: 用户chrylis-cautiouslyoptimistic 正确地指出我给出的代码可以输出具有顺序一致性的v1=1 v2=4 — 线程代码中的行只是应该交错一点点不同。

所以这里是稍微修改的代码(我已经改变了读取的顺序),其顺序一致性无法输出v1=1 v2=4,但一切仍然适用。

// Shared code
volatile int vv = 0;
int v1 = 0;
int v2 = 0;


// Thread1       Thread2
   v1 = 1;
   v2 = 2;
   vv = 10;      while(vv == 0) {;}
                 int r2 = v2;
                 int r1 = v1;
                 System.out.println("v1=" + r1 + " v2=" + r2);
   v1 = 3;
   v2 = 4;
   vv = 20;

【问题讨论】:

  • 你怎么知道线程的执行是如何在运行时交错的?
  • 你的意思是在上面的代码示例中吗?我只是举了一种可能的执行方式作为例子。代码示例的重点在于它与 Java 语言规范相矛盾:它符合他们的“顺序一致”标准,但允许违反它的执行。

标签: java java-memory-model jls


【解决方案1】:

您的错误在于要点 #1:v1v2 的读取 同步。

happens-before 关系由与vv 的交互创建,因此例如在这种情况下,如果您在开头添加了vv您的打印声明,您将保证看不到vv=20,v2=4。由于您忙于等待 vv 变为非零但随后不再与它交互,唯一的保证是您将看到在它变为非零之前发生的所有效果(分配1 和 2)。你也可能看到未来的影响,因为你没有任何进一步的发生之前

即使你将所有变量声明为 volatile,你仍然可以输出v1=1,v2=4 因为变量的多线程访问没有定义的顺序 em>,全局序列可以这样:

  1. T1:写v1=1
  2. T1:写v2=2
  3. T1:写vv=10(线程2不能在此之前退出while循环,保证能看到所有这些效果。)
  4. T2:读取vv=10
  5. T2:读取v1=1
  6. T1:写v1=3
  7. T1:写v2=4
  8. T2:读取v2=4

在每个步骤之后,内存模型保证所有线程都会看到相同的 volatile 变量值,但是您会遇到数据竞争,这是 因为访问不是原子的(分组的)时间>。为了确保您在一个组中看到它们,您需要使用一些其他方式,例如在synchronized 块中执行或将所有值放入记录类并使用volatileAtomicReference 进行交换出整条记录。

形式上,JLS 定义的数据竞争包括操作 T1(写入 v1=3)和 T2(读取 v1)(以及 v2 上的第二个数据竞争)。这些是冲突的访问(因为 T1 访问是写入),但是虽然这两个事件都发生在 T2(读取 vv)之后,但它们之间的顺序彼此之间没有关系.

【讨论】:

  • 我编辑了我的问题 - 我在末尾附加了一个稍微修改过的代码版本(我更改了 Thread2 中的读取顺序),现在应该看不到 v1=1 v2=4如果我将所有变量声明为 volatile。
  • 我想我理解你的解释并且同意几乎所有的事情。但看起来您对数据竞争的定义与 JLS 中的定义不同。而且我想了解它们在 JLS 中的含义——因为它是所有 Java 实现都必须遵守的规范。
  • @JavaCur 已更新以明确识别构成数据竞争的访问操作。
  • ...这不是一个定义的术语...你的意思是besides the ones here?
  • @Eugene 我已经阅读了多个 Java 版本的整个 JLS,但不知道我是如何忘记实际上定义了关系的。 (有问题的操作仍然不相关。)
【解决方案2】:

实际上证明你错了比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下“同步”,所有这些都在正确的chapter in the JSL 中定义。接受的答案说synchronizes-with 不是一个实际术语,但这是错误的。 (除非我没有理解意图或其中有错误)。

由于在Thread1Thread2 之间没有这样的特殊操作来建立同步顺序(简称SW),因此接下来的一切都像纸牌城堡一样倒塌,不再有意义。

您提到了volatile,但同时要注意subsequent 的含义:

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

这意味着读取将观察写入。


如果您更改代码并建立synchronizes-with 关系并隐含地因此happens-before 像这样:

  v1 = 1;
  v2 = 2;
  vv = 10; 

             if(vv == 10) {
                int r1 = v1;
                int r2 = v2;
                // What are you allowed to see here?
             }

您可以开始推理在 if 块中可以看到什么。你从简单的开始,from here:

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

好的,所以v1 = 1happens-beforev2 = 2happens-beforevv = 10。这样,我们在同一线程中的动作之间建立hb

我们可以通过synchronizes-with order、the proper chapter 和适当的规则“同步”不同的线程:

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

这样我们就在两个独立线程之间建立了SW 顺序。这反过来又允许我们现在构建一个HB(发生在之前),因为proper chapter 和另一个适当的规则:

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

所以现在你有一个链:

        (HB)          (HB)            (HB)                (HB)
v1 = 1 -----> v2 = 2 -----> vv = 10 ------> if(vv == 10) -----> r1 = v1 ....

所以只有现在,你有证据表明 if 块将读取 r1 = 1r2 = 2。而且因为volatile 提供顺序一致性(没有数据竞争),每个将读取vv10 的线程肯定也会将v1 读取为1v2 为@987654352 @。

【讨论】:

    猜你喜欢
    • 2011-04-20
    • 1970-01-01
    • 1970-01-01
    • 2021-11-02
    • 2021-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多