Java 中多次写入 volatile 变量的影响

effects of several writes on a volatile variable in Java

我在这个网站上发现了一些有趣的问题(例如,this one) about the visibility effects of volatile variables in Java originated from this paragraph taken from the book Java concurrency in Practice:

The visibility effects of volatile variables extend beyond the value of the volatile variable itself. When thread A writes to a volatile variable and subsequently thread B reads that same variable, the values of all variables that were visible to A prior to writing to the volatile variable become visible to B after reading the volatile variable. So from a memory visibility perspective, writing a volatile variable is like exiting a synchronized block and reading a volatile variable is like entering a synchronized block.

然而,即使在阅读了本网站相关问题的答案后,我仍然不完全清楚一个场景,具体来说:

线程 A0 在 线程 A[ 之前写入相同的易失性变量 会产生什么影响=47=]? 换句话说:A0 写入 volatile 变量,此值稍后被 A 覆盖( 不读取变量) 然后由 B 读取。 (所以我们有两个来自不同线程的写操作(A0A)和一个来自第三个线程的读操作(B )).

我可以安全地假设 AB 都能保证看到 一切A0 写入 volatile 变量之前对 A0 可见?

更新:

这是一个关于 Java 内存模型的概念性问题。 我知道我无法预测在线程 A0A 中写入 volatile 变量的顺序以及在线程 [=25= 中读取的顺序]B。 不过,为了方便讨论,还是说 A0A 开始之前启动了很多时间,并且在另一个重要的时间 B 开始之后,并让我们做一个简化的假设,即这足以保证写入和读取按描述的顺序发生(我知道顺序不能仅通过时间来保证,这只是为了避免与原始问题不同而进行的简化) .

Can I safely assume that both A and B are guaranteed to see everything that was visible to A0 before A0 wrote to the volatile variable?

写入 volatile 不会为线程提供任何关于读取的先行发生保证。

B 读取时,它将看到 A0 可以看到的任何内容,前提是它看到它所做的更新。 B 也可以看到任何 A 可以看到的东西,只要它看到 A 写的值。否则,如果它读取的值不是来自那个线程,即它太早,B 可能会看到比任一线程更早的状态。

如果 A0 尝试先写入,它可能会在 A 之后完成,例如假设 A 与数据的干净副本位于同一套接字上。它可以在 A0 之前访问它,因为后者需要更长的时间来获取缓存行。最后完成的那个决定了数据的最终状态。

我觉得场景很简单

A0 - 写入可变变量 VWA0

A - 写入可变变量 VWA

现在

Can I safely assume that both A and B are guaranteed to see everything that was visible to A0 before A0 wrote to the volatile variable?

如果线程 A 仅写入 V ,那么它可能会或可能不会看到写入 V 之前 A0 可见的所有内容。

只有线程A读取了变量V,读取到的值结果是WA0,才能保证线程A看到一切可见在写入 V.

之前到 A0

线程 B 也是如此,这取决于 B 在读取 V 的值后看到的是哪个值。

如果 B 将 V 的值读取为 WA,那么它将看到在线程 A 中写入 V 之前发生的所有事情。

如果 B 将 V 的值读取为 WA0,那么它将看到在线程 A0 中写入 V 之前发生的所有事情。

另外请记住

Thread A0 

  a = 1;   // non volatile write 
  V = WA0; // volatile write 

Thread A;
  a=3
  V = WA;  // volatile write 


Thread B;
  while(V == 'WA') {
    assert(a,3); // This may fail
  }

您需要了解线程 b 中的代码未正确同步 volatile 提供与可见性和重新排序相关的保证,但不保证原子性。

所以即使线程 B 读取 V 的值是 'WA' 并且它保证在写入 V 之前看到线程 A 中发生的所有事情,这并不一定意味着它会看到的值a 为 3,因为很可能在读取 V 的值后,线程 A0 将 a 作为 1 写入线程 B,从而使您的断言失败。 Happens before 向您保证在写入 v 之前必须发生的所有事情都已经发生,但这并不意味着您看不到未来的价值。 您可以通过执行类似的操作轻松重现该场景

Thread B;
      while(V == 'WA') {
        Thread.sleep(1000);
        assert(a,3); // This may fail
      }

因此,在这些情况下,Single Writer 更可取,否则您的程序不应在线程 B 中包含上述代码,因为这样您的程序中就会出现数据竞争。

编辑:

示例修改:

Thread 1 (T1)

   a = 1; // normal write
   b = 1; // normal write
   v = 1; // volatile write 

Thread 2 (T2)

   a = 2; // normal write
   c = 2; // normal write
   v = 2; // volatile write   

Thread 3  (T3)

  while(true) {
     if(v == 2) {

       assert (c == 2); //  will pass
       assert (b == 1); //  may fail if T1 hasn't run till
       assert (a == 2); //  may fail if T1 has run and set the value to 1
       break;
     }   
     if(v == 1) {
       assert (b == 1); // will pass
       assert (c == 2); // may fail if T2 hasn't run till
       assert (a == 1); // may fail if T2 has now run setting a == 2 
       break; 
     }
  }

" for the sake of facilitating the discussion, lets say that A0 starts lots of time before A does it, and after another significant amount of time B starts." as OP states

Now if i restate the above statement as (v == 1 happens before (hb) v == 2 ) .

i.e i assume i have a guarantee that v==1 hb (v==2)

然后线程 3 的行为将更改为

线程 3 (T3)

  while(true) {
     if(v == 2) {

       assert (c == 2); //  will pass
       assert (b == 1); //  will pass
       assert (a == 2); //  will pass
       break;
     }   
     if(v == 1) {
       assert (b == 1); // will pass
       assert (c == 2); // may fail if T2 hasn't run till
       assert (a == 1); // may fail if T2 has now run setting a == 2 
       break; 
     }
  }

上述行为的问题是你如何保证

v==1 hb (v==2)

我想如果你能理解如何建立上述保证,那么你自己就有答案了。

一种方法是建立@ishrat 在线程 A 中所做的方式。 jmm.

中还有其他方法

但是光靠时间是无法保证的,你需要依赖你所使用的语言规范和平台的底层保证。

也请阅读这篇优秀的文章article about jmm

A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order). JLS 17.4.4

因此,来自 A0 的写入 与来自 B 的读取 同步。所有来自 A0 的较早写入发生在 以下来自 B 的读取之前.如果没有其他人写这些变量,那么是的,B一定看到了。

我找不到A与A0同步。因此,无法保证 A 中的可见性。

根据更新的问题,我们有以下执行,其中 A0、A1 和 B 是线程,t0、t1、t2 是语句,writeV、readV 是 writes/reads 到 volatile V:

A0             A1              B
t0 <-- writeV     
         ^     t1 <-- writeV    
         |              ^
         |              \------readV <-- t2
         \-----------------------/

箭头表示发生在之前。我们有 t0 和 t1 happensbefore t2。 t2 将看到来自 t0 和 t1 的更新。

因此,是的:B 保证可以看到 A0 在 t0 中写入的所有内容。并且会读取A.

写入的volatile值

但是不是 t0 happensbefore t,反之亦然。 因此,不:不能保证 A 看到 A0 写的所有内容。

然而,这只是概念上的,在现实生活中B根本不知道A0是否执行了。

Can I safely assume that both A and B are guaranteed to see everything that was visible to A0 before A0 wrote to the volatile variable?

。对于易失性存储,当涉及到谓词时,就会构建先行发生的边缘。例如,为了让线程 A 看到 A0 的写入完成的所有写入,它需要在易失性写入上断言。

int a,b;
volatile c;
A0:
 a = 10;
 b = 5;
 c = 20;
A:
 if(c == 20){
    // here all writes by A0 are guaranteed to be visible (ie: a and b).
    c = 50;
 }

如果我们引入第三个线程 B 需要进行类似的检查:

int a,b;
volatile c;
A0:
 a = 10;
 b = 5;
 c = 20;
A:
 if(c == 20){ 
    c = 50;
 }

B:
  if(c == 50){
    // B will see all writes done by A and also A0.
  } 

因此,您正在寻找的保证与这些由写入和后续读取定义的先行边相关联。

否则,线程 A 和 B 可以读取 abc 作为它们的默认值 0 或写入的值。

However, for the sake of facilitating the discussion, lets say that A0 starts lots of time before A does it, and after another significant amount of time B starts.

在实践中,您可能不会 运行 遇到内存问题,但您永远不应该提前带着这种想法编写软件。

此外,我怀疑你是这个意思,但如果你真的是想 start - 一个线程的写入发生在之前另一个线程的开始(开始发生在同一个线程中)。


编辑您的评论。

如果我们删除来自 A 的读取但保留来自 B 的读取,则每个线程的可能值为

int a,b;
volatile c;
A0:
 a = 10;
 b = 5;
 c = 20;
A:
 int l = c; // c could be either 20 or 0
 int j = a; // a could be either 10 or 0
 int k = b; // b could be either 5 or 0
 c = 50;

B:
  if(c == 50){
     int l = c; // c can be 20 or 50. c cannot be 0
     int j = a; // a could be either 10 or 0
     int k = b; // b could be either 5 or 0
  } 

如您所见,线程 A0 的写入与 AB 的 reads/writes 之间没有关系,除非我们有读取前提条件.