发生在推理之前应用于一些例子

我们将提供一些示例来说明如何应用发生之前的推理来检查写入对后续读取是否可见。

单线程代码

正如你所期望的那样,写入对于单线程程序中的后续读取始终可见。

public class SingleThreadExample {
    public int a, b;
    
    public int add() {
       a = 1;         // write(a)
       b = 2;         // write(b)
       return a + b;  // read(a) followed by read(b)
    }
}

通过发生 - 在规则#1 之前:

  1. write(a) 动作发生在 write(b) 动作之前
  2. write(b) 动作发生在 read(a) 动作之前
  3. read(a) 动作发生在 read(a) 动作之前

通过发生 - 在规则#4 之前:

  1. write(a) 发生 - 在 write(b)write(b) 发生之前 - 在 read(a) IMPLIES write(a) 发生之前 - 发生之前 12。
  2. write(b) 发生 - 在 read(a)read(a) 发生之前 - 在 read(b) 之前发生了 write(b) 发生 - 在 read(b) 之前

加起来:

  1. write(a) 发生 - 在 read(a) 关系之前意味着 a + b 语句保证看到 a 的正确值。
  2. write(b) 发生 - 在 read(b) 关系之前意味着 a + b 语句保证看到 b 的正确值。

具有 2 个线程的示例中的 volatile 行为

我们将使用以下示例代码来探讨内存模型对于 volatile 的一些含义。

public class VolatileExample {
    private volatile int a;
    private int b;         // NOT volatile
    
    public void update(int first, int second) {
       b = first;         // write(b)
       a = second;         // write-volatile(a)
    }

    public int observe() {
       return a + b;       // read-volatile(a) followed by read(b)
    }
}

首先,考虑涉及 2 个线程的以下语句序列:

  1. 创建了一个 VolatileExample 实例; 叫它 ve
  2. ve.update(1, 2) 在一个线程中被调用,并且
  3. ve.observe() 在另一个线程中被调用。

通过发生 - 在规则#1 之前:

  1. write(a) 动作发生在 volatile-write(a) 动作之前
  2. volatile-read(a) 动作发生在 read(b) 动作之前

通过发生 - 在规则#2 之前:

  1. 第一个线程中的 volatile-write(a) 动作发生在第二个线程中的 volatile-read(a) 动作之前

通过发生 - 在规则#4 之前:

  1. 第一个线程中的 write(b) 动作发生在第二个线程中的 read(b) 动作之前

换句话说,对于这个特定的序列,我们保证第二个线程将看到第一个线程对非易失性变量 b 的更新。但是,还应该清楚的是,如果 update 方法中的赋值是相反的,或者 observe() 方法在 a 之前读取变量 b,那么之前发生的链将被破坏。如果第二个线程中的 volatile-read(a) 不在第一个线程中的 volatile-write(a) 之后,链也会被破坏。

当链断裂时,无法保证 observe() 能看到 b 的正确值。

挥发性有三个线程

假设我们在前面的例子中添加第三个线程:

  1. 创建了一个 VolatileExample 实例; 叫它 ve
  2. 两个线程称为 update
    • ve.update(1, 2) 在一个线程中被调用,
    • 在第二个帖子中调用 ve.update(3, 4)
  3. 随后在第三个线程中调用 ve.observe()

要完全分析这一点,我们需要考虑第一个线程和第二个线程中语句的所有可能交错。相反,我们只考虑其中两个。

场景#1 - 假设 update(1, 2)update(3,4) 之前我们得到这个序列:

write(b, 1), write-volatile(a, 2)     // first thread
write(b, 3), write-volatile(a, 4)     // second thread
read-volatile(a), read(b)             // third thread

在这种情况下,很容易看出从 write(b, 3)read(b) 之前有一个完整的发生链。此外,没有干预写入 b。因此,对于这种情况,第三个线程保证看到 b 具有值 3

场景#2 - 假设 update(1, 2)update(3,4) 重叠并且交错如下:

write(b, 3)                           // second thread
write(b, 1)                           // first thread
write-volatile(a, 2)                  // first thread
write-volatile(a, 4)                  // second thread
read-volatile(a), read(b)             // third thread

现在,虽然从 write(b, 3)read(b) 有一个发生在前的链,但是另一个线程执行了干预的 write(b, 1) 动作。这意味着我们无法确定 read(b) 会看到哪个值。

(旁白:这表明我们不能依赖 volatile 来确保非易失性变量的可见性,除非在非常有限的情况下。)