【问题标题】:Deep understanding of volatile in Java深入理解Java中的volatile
【发布时间】:2017-07-16 22:29:02
【问题描述】:

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。现在,我们有了预期的输出。 我的推理有什么不正确的?

【问题讨论】:

  • println 是一种同步方法 - 所以无论您要测试什么,都将从额外的同步中受益。您可能可以删除 volatile 关键字并获得相同的结果(取决于您的 CPU 等)...
  • 它怎么会输出1, 0?这些字段被初始化为0,然后在稍后分配1
  • 我已经编辑以避免混淆。
  • @assylias,为什么?
  • volatile 未根据内存屏障指定。如果您想深入了解volatile 和一般的Java 线程同步,请阅读Java Language Specification

标签: java volatile lock-free memory-barriers java-memory-model


【解决方案1】:

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

所以只有三种可能:

  • actor2 先运行,x 和 g 为 0 - 输出为 0,0
  • actor1 首先运行,x 和 g 为 1,因为发生在关系 HB 之前 - 输出为 1,1
  • 这些方法同时运行,并且只执行x=1(不是g=1),输出可以是0,1 或0,0(无易失性写入,因此无法保证)

【讨论】:

  • 您说“在 volatile 写入之前的任何操作都发生在对同一变量的任何后续 volatile 读取之前。”我不明白你。毕竟xg不是同一个变量。
  • x=1 是一个动作(由于程序顺序)发生在g=1 之前,这是一个易失性写入。 put_on_screen_without_sync(g); 是易失性读取。因此,在该语句之后对 x 的任何读取都将受益于 volatile 同步并返回 1。
  • 这是Java内存模型的基本保证。有关详细信息,请参阅 jls 的第 17 章。
  • @Gilgamesz 确切的意思是:在易失性写入之前发生的任何非易失性写入都将在易失性读取之后可见。
  • @assylias,你能看看*.com/questions/45151763/…吗?
【解决方案2】:

不,这是不可能的。根据 JMM,线程 1 在写入 volatile 字段时可见的任何内容在线程 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.
    }
  }
}

【讨论】:

    【解决方案3】:

    你永远不会看到1, 0,但正确解释这一点并不容易,规范明智。首先,让我们把一些显而易见的事情排除在外。规范says

    如果 x 和 y 是同一线程的操作,并且 x 在 程序顺序 中位于 y 之前,则为 hb(x, y)。

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

    在每个线程执行的所有线程间操作中...

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

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

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

    later 说:

    如果动作 x 后续动作 y 同步,那么我们也有 hb(x, y)。

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

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

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

    【讨论】:

      【解决方案4】:

      不,实际上volatile的这个属性在ConcurrentHashMap之类的类中是用来实现无锁快乐路径的,大致是这样的:

      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
          }
      }
      

      由于在 volatile 写入之前的写入不能在 volatile 写入之后重新排序,并且在 volatile 读取之后的读取不能在 volatile 读取之前重新排序,因此这样的代码确实可以用作“无锁锁定”。

      【讨论】: