12

据我所知volatile write 发生在 volatile read之前,所以我们总是会在 volatile 变量中看到最新的数据。我的问题基本上涉及“发生之前”一词以及它发生在哪里?我写了一段代码来澄清我的问题。

class Test {
   volatile int a;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            Thread.sleep(3000);
            t.a = 10;
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            System.out.println("Value " + t.a);
        }
     }).start();
   }
}

(为清楚起见,省略了 try catch 块)

在这种情况下,我总是看到要在控制台上打印值 0。如果没有Thread.sleep(3000);我总是看到值 10。这是发生在关系之前的情况,还是打印“值 10”,因为线程 1 更早地启动了线程 2?

很高兴看到每个程序启动时带有和不带有 volatile 变量的代码的行为都不同的示例,因为上面代码的结果仅取决于(至少在我的情况下)线程的顺序和线程休眠。

4

6 回答 6

9

您会看到值 0,因为读取是在写入之前执行的。您会看到值 10,因为写入是在读取之前执行的。

如果您想进行具有更多不可预测输出的测试,您应该让两个线程都等待 CountDownLatch,以使它们同时启动:

final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            t.a = 10;
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 new Thread(new Runnable(){
    @Override
    public void run() {
        try {
            latch.await();
            System.out.println("Value " + t.a);
        }
        catch (InterruptedException e) {
            // end the thread
        }
    }
 }).start();
 Thread.sleep(321); // go
 latch.countDown();
于 2012-06-04T21:07:36.973 回答
5

发生之前确实与写入发生在任何后续读取之前有关。如果写入尚未发生,则确实没有关系。由于写线程正在休眠,所以在写之前执行读。

要观察实际的关系,您可以有两个变量,一个是易失的,一个不是。根据 JMM,它说在易失性写入之前写入非易失性变量发生在易失性读取之前。

例如

volatile int a = 0;
int b = 0;

线程 1:

b = 10;
a = 1;

线程 2:

while(a != 1);
if(b != 10)
  throw new IllegalStateException();

Java 内存模型说b应该始终等于 10,因为非易失性存储发生在易失性存储之前。并且在 volatile 存储之前发生在一个线程中的所有写入都发生在所有后续的 volatile 加载之前。

于 2012-06-04T21:09:11.437 回答
1

不要坚持“发生在之前”这个词。它是事件之间的关系,由 jvm 在 R/W 操作调度期间使用。在这个阶段,它不会帮助你理解 volatile。关键是:jvm 命令所有 R/W 操作。jvm 可以随心所欲地订购(当然要遵守所有同步、锁定、等待等)。现在:如果变量是易失性的,那么任何读取操作都将看到最新写入操作的结果。如果变量不是易失的,则不能保证(在不同的线程中)。就这样

于 2012-06-04T21:16:50.913 回答
1

我已经重新措辞(更改粗体字体)您问题的第一句话中提到的发生之前的规则如下,以便可以更好地理解 -

“将volatile 变量的值写入主存储器 发生在随后从主存储器读取该变量之前”。

  • 同样重要的是要注意,易失性写入/读取总是发生在主内存中,而不是来自任何本地内存资源,如寄存器、处理器缓存等。

上述happens-before规则的实际含义是共享一个volatile变量的所有线程将始终看到该变量的一致值。没有两个线程在任何给定时间点看到该变量的不同值。

相反,共享非易失性变量的所有线程可能在任何给定时间点看到不同的值,除非它没有通过任何其他类型的同步机制(例如同步块/方法、final 关键字等)进行同步。

现在回到您关于此规则之前发生的问题,我认为您稍微误解了该规则。该规则并未规定写入代码应始终在读取代码之前发生(执行)。相反,它规定如果写入代码(易失性变量写入)要在另一个线程中的读取代码之前在一个线程中执行,那么写入代码的效果应该在读取代码执行之前发生在主存储器中,以便读取代码可以看到最新的值。

在没有 volatile(或任何其他同步机制)的情况下,这发生在之前不是强制性的,因此读取器线程可能会看到非 volatile 变量的陈旧值,即使它最近已由不同的写入器线程写入。因为写入器线程可以将值存储在其本地副本中,而无需将值刷新到主存储器。

希望上面的解释清楚:)

于 2013-02-18T11:27:00.337 回答
0

piotrek 是对的,这是测试:

class Test {
   volatile int a = 0;
   public static void main(String ... args) {
     final Test t = new Test();
     new Thread(new Runnable(){
        @Override
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (Exception e) {}
            t.a = 10;
            System.out.println("now t.a == 10");
        }
     }).start();
     new Thread(new Runnable(){
        @Override
        public void run() {
            while(t.a == 0) {}
            System.out.println("Loop done: " + t.a);
        }
     }).start();
   }
}

with volatile: 它总会结束

没有 volatile:它永远不会结束

于 2012-08-17T00:35:54.663 回答
0

来自维基:

特别是在 Java 中,happens-before 关系保证语句 A 写入的内存对语句 B 可见,也就是说,语句 A 在语句 B 开始读取之前完成其写入。

因此,如果线程 A 用值 10 写入 ta 并且线程 B 稍后尝试读取 ta,那么发生之前的关系保证线程 B 必须读取线程 A 写入的值 10,而不是任何其他值。这很自然,就像爱丽丝买牛奶并把它们放进冰箱然后鲍勃打开冰箱看到牛奶一样。但是,当计算机运行时,内存访问通常不会直接访问内存,这太慢了。相反,软件从寄存器或缓存中获取数据以节省时间。它仅在发生缓存未命中时才从内存中加载数据。问题发生了。

让我们看看问题中的代码:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        System.out.println("Value " + t.a);
      }
    }).start();
  }
}

线程 A 将 10 写入值 ta 并且线程 B 尝试将其读出。假设线程 A 在线程 B 读取之前写入,那么当线程 B 读取时,它将从内存中加载值,因为它没有将值缓存在寄存器或缓存中,所以它总是得到 10 由线程 A 写入。如果线程 A 在之后写入线程 B 读取,线程 B 读取初始值 (0)。所以这个例子没有展示 volatile 的工作原理和区别。但是如果我们像这样更改代码:

class Test {
  volatile int a;
  public static void main(String ... args) {
    final Test t = new Test();
    new Thread(new Runnable(){ //thread A
      @Override
      public void run() {
        Thread.sleep(3000);
        t.a = 10;
      }
    }).start();
    new Thread(new Runnable(){ //thread B
      @Override
      public void run() {
        while (1) {
          System.out.println("Value " + t.a);
        }
      }
    }).start();
  }
}

如果没有volatile,即使在线程 A 将 10 写入 ta 之后发生某些读取,打印值也应始终为初始值 (0),这违反了之前发生的关系。原因是编译器优化了代码并将 ta 保存到寄存器中,每次它都会使用寄存器值而不是从缓存中读取,当然这要快得多。但这也导致了happens-before关系违规问题,因为线程B在其他人更新后无法获得正确的值。

在上面的例子中,易失性写入发生在易失性读取之前意味着使用易失性线程B将在线程A更新它之后获得正确的ta值一次。编译器将保证每次线程 B 读取 ta 时,它必须从缓存或内存中读取,而不仅仅是使用寄存器的陈旧值。

于 2013-05-31T12:23:23.453 回答