深入理解 Java 中的 volatile

Deep understanding of volatile in Java

Java是否允许输出1, 0?我已经对其进行了非常深入的测试,但无法获得该输出。我只得到 1, 10, 00, 1.

public class Main {
    private int x;
    private volatile int g;

    // Executed by thread #1
    public void actor1(){
       x = 1;
       g = 1;
    }

    // Executed by thread #2
    public void actor2(){
       put_on_screen_without_sync(g);
       put_on_screen_without_sync(x);
    }
}

为什么?

在我看来,有可能得到 1, 0。我的推理。 g 是易失性的,因此会确保内存顺序。所以,它看起来像:

actor1:

(1) store(x, 1)
(2) store(g, 1)
(3) memory_barrier // on x86

而且,我看到了以下情况: 在 store(x,1) 之前重新排序 store(g, 1)(memory_barrier 是 (2) 之后)。 现在,运行 线程 #2。所以,g = 1, x = 0。现在,我们有了预期的输出。 我的推理有什么不正确的地方?

易失性写入之前的任何操作发生在 (HB) 同一变量的任何后续易失性读取之前。在您的情况下,写入 x 发生在写入 g 之前(由于程序顺序)。

所以只有三种可能:

  • actor2 运行s first and x and g are 0 - output is 0,0
  • actor1 运行s first 并且 x 和 g 是 1 因为 happens before 关系 HB - 输出是 1,1
  • 方法 运行 并发并且只执行 x=1(不是 g=1)并且输出可以是 0,1 或 0,0(没有易失性写入,所以不能保证)

不,这不可能。根据 JMM,线程 1 在写入易失性字段时可见的任何内容在线程 2 读取该字段时都变得可见。

还有一个例子与你的相似provided here:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

不是,实际上volatile的这个属性是在类中使用的,就像ConcurrentHashMap一样,实现了一个无锁的happy path,大致是这样的:

volatile int locked = 0;
...
void mutate() {
    if (Unsafe.compareAndSwapInt(locked,0,1)) { 
    /*this isn't exactly how you call this method, but the point stands: 
      if we read 0, we atomically replace it with 1 and continue on the happy 
      path */
       //we are happy
       //so we mutate the structure and then
       locked = 0;           
    } else {
       //contended lock, we aren't happy
    }
}

由于在易失性写入之前的写入不能在易失性写入之后重新排序,并且易失性读取之后的读取不能在易失性读取之前重新排序,所以这样的代码确实可以作为 "lockless locking"。

你永远不会看到 1, 0,但正确解释这并不容易,具体而言。首先让我们把一些明显的东西拿出来。规范 says:

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

这意味着在写入线程侧,hb(x, g)读取hb(g, x)。但仅此而已,如果您必须对每个线程进行推理 individually, as the chapter about Program order says::

Among all the inter-thread actions performed by each thread t...

因此,如果您想象 运行 一次连接每个线程,那么 happens-before 对每个线程都是正确的。但你没有。您的演员(我确定您在那里使用 jcstress)同时 运行。所以靠“程序顺序”来推理是不够的(也不正确)。

您现在需要以某种方式同步这两个动作 - 阅读写作。这里是 how the specification says it can be done:

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

并且later说:

If an action x synchronizes-with a following action y, then we also have hb(x, y).

如果现在将所有这些放在一起:

          (hb)              (hb)             (hb)
write(x) ------> write(g) -------> read(g) -------> read(x)

这也被称为“传递”关闭program ordersynchronizes-with order。由于每一步都有 hb,因此根据规范不可能看到 1, 0(一个活泼的阅读)。