java内存模型:关于顺序一致性的jls语句似乎不正确

2fjabf4q  于 2021-06-29  发布在  Java
关注(0)|答案(2)|浏览(268)

我在读第17章。jls的线程和锁以及以下关于java中顺序一致性的声明在我看来是不正确的:
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。
他们将数据竞争定义为:
当一个程序包含两个冲突的访问时(§17.4.1)不按“发生在”关系排序的,称为包含数据竞争。
它们将冲突访问定义为:
如果对同一变量的两次访问(读或写)中至少有一次是写的,则称为冲突。
最后,他们有以下关于恋爱前发生的事情:
对易失性字段的写入(§8.3.1.4)在该字段的每次后续读取之前发生。
我对第1条语句的问题是,我想我可以想出一个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中的写入同步
can输出 v1=1 v2=4 (这违反了顺序一致性)。
因此,jls最初的声明
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的。
对我来说似乎不正确。
我是遗漏了什么还是在什么地方犯了错?
编辑:用户chrylis谨慎的optimistic正确地指出我给出的代码可以输出 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;
kmbjn2e3

kmbjn2e31#

你的错误在要点1:读取 v1 以及 v2 不与同步。
只有通过与客户的交互才能建立关系 vv ,例如在本例中 vv 在打印声明的开头,您肯定不会看到 vv=20,v2=4 . 既然你忙着等 vv 变为非零,但之后不再与它交互,唯一的保证是您将看到它变为非零之前发生的所有效果(1和2的赋值)。你也可以看到未来的影响,因为你没有任何进一步的事情发生之前。
即使您将所有变量声明为volatile,您仍然可以输出 v1=1,v2=4 因为变量的多线程访问没有定义的顺序,全局序列可以如下所示:
t1:写入 v1=1 t1:写入 v2=2 t1:写入 vv=10 (线程2在此之前无法退出while循环,并且保证可以看到所有这些效果。)
t2:读取 vv=10 t2:读取 v1=1 t1:写入 v1=3 t1:写入 v2=4 t2:读取 v2=4 在这些步骤中的每一步之后,内存模型保证所有线程都将看到相同的volatile变量值,但是您有一个数据竞争,这是因为访问不是原子的(分组的)。为了确保您在组中看到它们,您需要使用其他一些方法,例如在 synchronized 阻止或将所有值放入一个记录类中,并使用 volatile 或者 AtomicReference 把整张唱片换掉。
形式上,由jls定义的数据竞争由操作t1(write)组成 v1=3)和t2(读取 v1)(以及v2上的第二个数据竞赛)。这些是相互冲突的访问(因为t1访问是一个写访问),但是这两个事件都发生在t2(读)之后 vv),它们之间没有相互关联的顺序。

uxh89sit

uxh89sit2#

事实上证明你错了比你想象的要容易得多。两个独立线程之间的操作在非常特殊的规则下是“同步的”,所有这些规则都在jsl的适当章节中定义。公认的答案是 synchronizes-with 不是一个实际的术语,但那是错误的(除非我没有理解意图或其中有错误)。
因为你没有这样的特殊动作来建立同步订单( SW 简而言之),介于 Thread1 以及 Thread2 ,接下来的一切就像一座纸牌城堡,不再有意义。
你提到 volatile 但同时要小心什么 subsequent 意味着:
对易失性字段的写入发生在该字段的每次后续读取之前。
它意味着一个读将观察到写。
如果你改变你的代码并建立一个 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块中可能看到的内容。你可以从这里简单地开始:
如果x和y是同一线程的动作,并且x按程序顺序排在y之前,那么hb(x,y)。
好吧,那么 v1 = 1 happens-before v2 = 2 以及
happens-before vv = 10 . 这样我们就建立了 hb 在同一线程中的操作之间。
我们可以通过“同步”不同的线程 synchronizes-with 通过适当的章节和适当的规则:
对volatile变量v的写入与任何线程对v的所有后续读取同步
这样我们就建立了一个 SW 两个独立线程之间的顺序。这反过来又使我们能够建立一个 HB (以前发生过)现在,由于这一章和另一条规则:
如果一个动作x与后面的动作y同步,那么我们还有hb(x,y)。
现在你有了一条链条:

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

所以直到现在,你才有证据证明,如果block r1 = 1 以及 r2 = 2 . 因为 volatile 提供顺序一致性(无数据争用),每个线程都将读取 vv 成为 10 当然,威尔也会读 v1 成为 1 以及 v2 成为 2 .

相关问题