问题 在java中易失性


我所知道的 易失写  之前发生  易读,所以我们总会看到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变量的代码行为都不同的例子,因为上面代码的结果仅取决于(至少在我的情况下)线程顺序和线程休眠。


3951
2018-06-04 21:00


起源

我可能在这里错了,但AFAIK volatile关键字实质上告诉JVM它不能按访问变量的顺序进行任何更改,以尝试优化。换句话说,您会以潜在的等待为代价获得安全性(如在串行执行中)。我可能错了,所以看看其他人对这个问题有什么看法会很有意思。 - posdef
可能重复 Java:volatile保证和乱序执行 - Woot4Moo
唯一不稳定的野心是任何读取变量的线程都会看到最近写入的值。挥发性修饰剂主要用于多个线程。 Java允许线程可以保留共享变量(缓存)的私有工作副本。需要使用主存储器中的主副本更新这些工作副本。易失性意味着变量将存在于主存储器中 只要 不在私人工作副本(缓存)。 - Justin


答案:


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

9
2018-06-04 21:07



+1好的例子,加上小费 CountDownLatch - posdef
哇好主意 CountDownLatch。像“反转”信号量这样的东西也可能有效。 - Sebastian Hoffmann
即使我使用CountDaownlatch同时启动线程,volotile也不能按预期工作:每个程序调用得到不同的结果(注意:我的处理器有2个内核) - maks
@maks这个例子的问题是它实际上并没有证明发生在之前的关系 volatile a。挥发性在这里没有任何作用。您需要拥有第一个线程集 t.a 然后 countDown 而另一个线程等待 latch.await()  这会在CountDownLatch上的两个线程之间创建一个before-before关系。 - John Vint


发生之前真正与写入发生在任何后续读取之前。如果写没有发生,那真的没有关系。由于写线程处于休眠状态,因此在写入发生之前执行读取。

要观察行动中的关系,你可以有两个变量,一个是volatile,另一个是volatile。根据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存储之前。并且所有写入都发生在易失性存储发生之前的一个线程中 - 在所有后续的易失性加载之前。


4
2018-06-04 21:09



+1这正是在手段之前发生的事情。有关更详细的讨论,请参阅: jeremymanson.blogspot.co.uk/2008/11/... 代码中的示例和代码中并发边缘情况的探索请参见此处: github.com/shipilev/java-concurrency-torture - Nitsan Wakart


我已经重新措辞(粗体字的变化)你问题的第一句中提到的先前发生的规则,如下所示,以便更好地理解 -

“写 volatile变量的值对主存储器的影响    之前发生 随后从主存中读取该变量”。

  • 另外值得注意的是 易失性写/读总是 发生在/从主存储器发生  往返任何本地记忆 寄存器,处理器缓存等资源

上述事实的实际意义发生在规则之前 共享volatile变量的所有线程将始终看到一致的值 那个变量。没有两个线程在任何给定的时间点看到该变量的不同值。

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

现在回到你关于这个发生前的规则的问题,我想你有点误解了这条规则。该规则并未规定写代码应始终在读取代码之前发生(执行)。相反,它规定如果在另一个线程中的读取代码之前在一个线程中执行写入代码(volatile variable write),那么写入代码的效果应该具有 发生 在主存中 之前 执行读取代码,以便读取代码可以看到最新值。

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

希望以上解释清楚:)


1
2018-02-18 11:27





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


1
2018-06-04 21:16



JVM无法重新排序在写入之后进行易失性写入之前所做的写入,这就是在意味着之前发生的事情。它们必须发生在之前。这是JMM的一部分,如果破坏了,你的JVM中就会出现一个bug。你如何使用它是另一回事。 - Nitsan Wakart
你是对的。我只是说'发生在'之前'的概念对初学者理解volatile的含义并没有多大帮助 - piotrek


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:它将永远结束

没有不稳定:永远不会结束


0
2017-08-17 00:35



没有波动,不能保证它会永远结束,但它可能会结束。 - assylias


来自维基:

特别是在Java中,先发生关系是对语句A写入的内存对语句B可见的保证,即语句A在语句B开始读取之前完成其写入。

因此,如果线程A写入值为10的t.a而线程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读取它时将从内存加载该值,因为它不会将值缓存在寄存器或缓存中,因此它总是由线程A写入10。如果线程A写入之后线程B读取,线程B读取初始值(0)。所以这个例子没有显示易变性的工作原理和差异。但是如果我们改变这样的代码:

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

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

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


0
2018-05-31 12:23