【问题标题】:Volatile in javajava中的易失性
【发布时间】:2012-06-04 21:00:47
【问题描述】:

据我所知volatile write happens-before volatile read,所以我们总是会在 volatile 变量中看到最新的数据。我的问题主要涉及 happens-before 这个词,它发生在哪里?我写了一段代码来澄清我的问题。

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 变量的代码的行为都不同的示例,因为上面代码的结果仅取决于(至少在我的情况下)线程的顺序和线程睡觉。

【问题讨论】:

  • 我在这里可能错了,但 AFAIK volatile 关键字本质上告诉 JVM,它不能对变量的访问顺序进行任何更改,以尝试优化。换句话说,您以潜在的等待为代价获得了安全性(如串行执行)。我可能是错的,所以看看其他人对此事的看法会很有趣。
  • 唯一的 volatile 保证是任何读取变量的线程都会看到最近写入的值。 volatile修饰符主要用于多线程。 Java 允许线程可以保留共享变量(缓存)的私有工作副本。这些工作副本需要使用主存储器中的主副本进行更新。易失性意味着变量将存在于主内存中,而不存在于私有工作副本(缓存)中。

标签: java concurrency volatile


【解决方案1】:

您会看到值 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();

【讨论】:

  • CountDownLatch 真是个好主意。类似“反向”信号量的东西也可能起作用。
  • 即使我同时使用 CountDaownlatch 启动线程,volotile 也无法按预期工作:每次程序调用我得到不同的结果(注意:我的处理器有 2 个内核)
  • @maks 这个例子的问题是它实际上并没有展示关于volatile a 的发生之前的关系。 Volatile 在这里没有任何用途。您需要将第一个线程设置为t.a,然后设置countDown,而另一个线程在latch.await() 上等待,这会在 CountDownLatch 上的两个线程之间创建发生前的关系。
【解决方案2】:

happens-before 确实与写入发生在任何后续读取之前有关。如果写入尚未发生,则确实没有关系。由于写入线程处于休眠状态,因此在写入发生之前执行读取。

要观察实际的关系,您可以有两个变量,一个是易失的,一个不是。根据 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 加载之前。

【讨论】:

【解决方案3】:

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

“将易失性变量的值写入主存发生在随后从主存中读取该变量”。

  • 另外需要注意的是,volatile 写入/读取总是 发生在主内存/从主内存,NOT 发生在/从任何本地内存 寄存器、处理器缓存等资源。

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

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

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

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

希望上面的解释清楚:)

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • JVM 无法重新排序在 volatile 写入之前进行的写入,以便在该写入之后进行,这就是在手段之前发生的情况。它们必须发生在之前。这是 JMM 的一部分,如果它被破坏,您的 JVM 中就会出现错误。你如何使用它是另一回事。
    • 是的,你是对的。我只是说“之前发生”的概念对于初学者理解 volatile 的含义并没有真正的帮助
    【解决方案5】:

    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();
       }
    }
    

    带有易失性:它总是会结束

    没有 volatile:它永远不会结束

    【讨论】:

    • 没有 volatile,不能保证它会永远结束,但它可能会结束。
    【解决方案6】:

    来自维基:

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

    因此,如果线程 A 将 t.a 写入值为 10,而线程 B 稍后尝试读取 t.a,则发生之前的关系保证线程 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 写入值 t.a 并且线程 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,打印值应该始终为初始值(0),即使在线程A将10写入t.a之后发生了一些读取,这违反了happens-before关系。原因是编译器优化了代码并将 t.a 保存到寄存器中,每次它都会使用寄存器值而不是从缓存中读取,当然这要快得多。但这也导致了happens-before关系违规问题,因为线程B在其他人更新后无法获得正确的值。

    在上面的例子中,volatile writehappens-before volatile read 意味着线程 B 在线程 A 更新它之后,线程 B 将获得正确的 t.a 值一次。编译器会保证每次线程 B 读取 t.a 时,它必须从缓存或内存中读取,而不是仅仅使用寄存器的陈旧值。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-07-07
      • 1970-01-01
      • 1970-01-01
      • 2011-01-26
      • 2016-05-31
      • 2012-02-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多