java 如何使用volatile来确保顺序一致性

vohkndzv  于 2023-06-20  发布在  Java
关注(0)|答案(3)|浏览(110)

考虑两个线程:
A==B==0(初始)
| 线程1|线程2|
| - -----|- -----|
| B=42;| if(A==1)|
| A=1;|...print(B)|
据我所知,如果(至少)Avolatile,我们将只能在print处读取B==42。虽然如果只将B标记为volatile,我们可以读取B==42,但也可以读取B==0
我想更仔细地看看只有Bvolatile的情况,并理解为什么我们可以根据这些文档所说的来读取B==0。为此,我开始添加所有程序顺序边缘并与文档中所述同步:

B=42A=1的两条边是简单程序顺序(PO)边,其余边与(SW)边同步。根据文档,当“* 对每个变量的默认值[...]的写入与每个线程中的第一个操作同步。”(这些是图片中的前4个边缘)和“ 对易失性变量v [...]的写入与v* 的所有后续读取同步”(从B=42print(B)的边缘)时,我们有一个SW边缘。
现在,我们可以看看在边存在(HB)之前发生了什么,根据文档,这些边中的每一个也是HB排序。此外,对于所有hb(x,y)和hb(y,z),我们有hb(x,z)(这些边丢失,但我们仍然使用它们)。
最后,我们从文档中获得了我们可以在print(B)上读取的内容:“我们说,如果在happens-before偏序[...]中,变量v的读r被允许观察到写w到v:

  • r不在w之前排序(即,hb(r,w)不是这样),并且
  • 不存在[...]对v的写入w'使得hb(w,w')和hb(w ',r)“

让我们看看是否可以在读r(print(B))时观察到写w(B=0)。我们确实没有hb(r,w)。然而,我们确实具有插入hb(wow’)和hb(w’,r)的写入w’(B=42)。
这让我想知道我们是否能在打印时观察到B==0,如果是的话,我对文档的推理或理解错在哪里?我想要一个明确的答案是指文件。
(我已经看过这个post,但是我希望得到一个更接近JMM文档的解释,我的问题也来自这个特定的代码)

lrpiutwd

lrpiutwd1#

为了确保我理解正确:

  • 你的问题是

这让我想知道我们是否可以在打印时观察到B==0,如果是的话,我的推理或对文档的理解错在哪里?

  • Java代码是:
package my.test;

class MyTest {

  static int A; // =0
  static volatile int B; // =0

  public static void main(String[] args) throws InterruptedException {
    var t1 = new Thread(() -> {
      B = 42;
      A = 1;
    });
    var t2 = new Thread(() -> {
      if (A == 1) {          // reads A==1 
        System.out.print(B); // reads B==0
      }
    });

    t1.start();
    t2.start();

    t1.join();
    t2.join();
  }
}

我们对执行感兴趣,其中:

  • A == 1A读取器读取1
  • System.out.print(B)B读取器读取0

就JMM操作而言,执行如下(为简洁起见,仅显示了AB上的操作):

Initially:
         write(A=0)
volatile-write(B=0)

Thread1:                Thread2:
volatile-write(B=42)             read(A):1
         write(A=1)     volatile-read(B):0

下面是执行中动作之间的 program-ordersynchronizes-with(图中的[po][sw])关系:

write(A=0)                         
            ↓[po]                           
volatile-write(B=0)                         
            ││└──────────────────────┐      
            │└───────────────┐       │      
            ↓[sw]            │       ↓[sw]  
volatile-write(B=42)         │     read(A):1
            ↓[po]            ↓[sw]   ↓[po]  
         write(A=1)       volatile-read(B):0

注意事项:

  • 由于以下规则,在每个线程中的初始写入和第一个操作之间存在[sw]边缘:

将默认值(零、假或空)写入每个变量 * 与 * 每个线程中的第一个操作同步。

Although it may seem a little strange to write a default value to a variable before the object containing the variable is allocated, conceptually every object is created at the start of the program with its default initialized values.
  • volatile-read(B):0 * 与 * volatile-write(B=0)同步,因为以下规则:

对volatile变量 v(§8.3.1.4)的写操作 * 与 * 任何线程对 v 的所有后续读取同步(其中“后续”是根据同步顺序定义的)。
顺便说一句,在我们的例子中,同步顺序是:volatile-write(B=0) -> volatile-read(B):0 -> volatile-write(B=42)
根据同步顺序的定义,它是所有同步动作之间的全局顺序,与程序顺序一致。
读取volatile-read(B):0返回0,因为以下规则:
执行遵循同步顺序一致性。
对于 A 中的所有易失性读取 r,不是 so(r,W(r))A 中存在写入 w 使得 w.v = r.vso(W(r),w)so(w,r) 的情况。

  • volatile-write(B=42)volatile-read(B):0之间没有[sw]边。这是因为根据下面的规则,volatile写操作只与变量的volatile读操作同步,这些读操作在同步顺序中较晚

对volatile变量v(§8.3.1.4)的写入 * 与 * 任何线程对v的所有后续读取同步(其中“后续”是根据同步顺序定义的)。

  • write(A=1)read(A):1之间没有[sw]边缘,因为它们是普通的写入和读取,但是[sw]仅用于同步操作

下面是动作之间的 happens-before 关系(由[po][sw]构建):

write(A=0)                         
            ↓[hb]                           
volatile-write(B=0)                         
            │└──────────────────────┐       
            ↓[hb]                   ↓[hb]   
volatile-write(B=42)              read(A):1 
            ↓[hb]                   ↓[hb]   
         write(A=1)       volatile-read(B):0

根据JLS,发生在一致性之前:
我们说一个变量v的读r被允许观察一个wv的写,如果,在执行跟踪的 happens-before 部分顺序中:
r不排序在w之前(即,不是hb(r, w)的情况),并且
不存在插入写入w'v(即,no write w' to v so that hb(w, w') and hb(w', r)).
非正式地,如果没有happens-before命令来阻止读取,则允许读取r查看写入w的结果。
一组动作A是happens-before一致的,如果对于A中的所有读取r,其中W(r)r看到的写入动作,情况不是hb(r, W(r))或者在A中存在写w,使得w.v = r.vhb(W(r), w)hb(w, r)
在happens-before一致的操作集合中,每个读都看到happens-before排序允许它看到的写。
对于read(A):1,Happens-before一致性没有被破坏,因为正如您在上面的图表中所看到的,write(A=1)read(A):1之间没有happens-before关系。
volatile-read(B):0也很好(上面解释过)。
事实上,我在JMM中没有看到任何在这次执行中违反的东西--所以根据JMM,IMO的执行是法律的的。

0md85ypi

0md85ypi2#

我相信你的误解是写B=42。由于线程2直到print(B)语句才读取B,因此在读取A之前,B没有happens before关系。因此,B=42的写入不会影响线程2对A的读取。
因此线程2可以在B=42写入之前观察到A=1的写入。

s3fp2yjn

s3fp2yjn3#

B=42A=1的两条边是简单程序顺序(PO)边
使B易失性保证了线程1在B=42赋值之前 * 按程序顺序 * 所做的任何事情,在线程2从B读取42时,必须对线程2可见。但是,在您的示例中,线程1在分配B=42之前什么都不做。所以,* nothing * 就是volatile read所保证的。
有件事可能会发生。编译器可以重新排序线程1中的两个赋值,或者硬件可以乱序存储值。如果它不改变线程1本身发生的任何事情的结果,那么它就不违反“程序顺序”。所以,事情可能会这样发展:

Thread 1      Thread 2
A = 1;
              if (A==1) {                  // succeeds!
                  System.out.println(B);   // prints "0"
B = 42;       }

我不知道为什么这些作业会被重新排序,但我很确定规则允许它们被重新排序。

相关问题