简体   繁体   English

为什么标记Java变量volatile会降低同步性?

[英]Why does marking a Java variable volatile make things less synchronized?

So I just learned about the volatile keyword while writing some examples for a section that I am TAing tomorrow. 所以我刚刚学习了volatile关键字,同时为明天的TAing部分编写了一些示例。 I wrote a quick program to demonstrate that the ++ and -- operations are not atomic. 我写了一个快速程序来证明++和 - 操作不是原子的。

public class Q3 {

    private static int count = 0;

    private static class Worker1 implements Runnable{

        public void run(){
            for(int i = 0; i < 10000; i++)
                count++; //Inner class maintains an implicit reference to its parent
        }
    }

    private static class Worker2 implements Runnable{

        public void run(){
            for(int i = 0; i < 10000; i++)
                count--; //Inner class maintains an implicit reference to its parent
        }
    }


    public static void main(String[] args) throws InterruptedException {
        while(true){
            Thread T1 = new Thread(new Worker1());
            Thread T2 = new Thread(new Worker2());
            T1.start();
            T2.start();

            T1.join();
            T2.join();
            System.out.println(count);
            count = 0;
            Thread.sleep(500);

        }
    }
}

As expected the output of this program is generally along the lines of: 正如预期的那样,该计划的产出大致如下:

-1521
  -39
    0
    0
    0
    0
    0
    0

However, when I change: 但是,当我改变时:

private static int count = 0;

to

private static volatile int count = 0;

my output changes to: 我的输出更改为:

    0
 3077
    1
-3365
   -1
   -2
 2144
    3
    0
   -1
    1
   -2
    6
    1
    1

I've read When exactly do you use the volatile keyword in Java? 我读过你什么时候在Java中使用volatile关键字? so I feel like I've got a basic understanding of what the keyword does (maintain synchronization across cached copies of a variable in different threads but is not read-update-write safe). 所以我觉得我已经对关键字的作用有了基本的了解(在不同的线程中保持变量的缓存副本的同步,但不是read-update-write safe)。 I understand that this code is, of course, not thread safe. 我知道这段代码当然不是线程安全的。 It is specifically not thread-safe to act as an example to my students. 对我的学生来说,作为一个例子,特别不是线程安全的。 However, I am curious as to why adding the volatile keyword makes the output not as "stable" as when the keyword is not present. 但是,我很好奇为什么添加volatile关键字使得输出不像关键字不存在时那样“稳定”。

Why does marking a Java variable volatile make things less synchronized? 为什么标记Java变量volatile会降低同步性?

The question "why does the code run worse" with the volatile keyword is not a valid question. 使用volatile关键字“为什么代码运行更糟”的问题不是一个有效的问题。 It is behaving differently because of the different memory model that is used for volatile fields. 由于用于易失性字段的不同内存模型,它的行为不同 The fact that your program's output tended towards 0 without the keyword cannot be relied upon and if you moved to a different architecture with differing CPU threading or number of CPUs, vastly different results would not be uncommon. 您的程序输出在没有关键字的情况下趋于0的事实无法依赖,如果您转移到具有不同CPU线程或CPU数量的不同体系结构,则不同的结果并不罕见。

Also, it is important to remember that although x++ seems atomic, it is actually a read/modify/write operation. 此外,重要的是要记住虽然x++似乎是原子的,但它实际上是一个读/修改/写操作。 If you run your test program on a number of different architectures, you will find different results because how the JVM implements volatile is very hardware dependent. 如果在许多不同的体系结构上运行测试程序,您会发现不同的结果,因为JVM实现volatile方式与硬件有关。 Accessing volatile fields can also be significantly slower than accessing cached fields -- sometimes by 1 or 2 orders of magnitude which will change the timing of your program. 访问volatile字段也可能比访问缓存字段慢得多 - 有时会增加1或2个数量级,这将改变程序的时间。

Use of the volatile keyword does erect a memory barrier for the specific field and (as of Java 5) this memory barrier is extended to all other shared variables. 使用volatile关键字会为特定字段建立内存屏障,并且(从Java 5开始)此内存屏障将扩展到所有其他共享变量。 This means that the value of the variables will be copied in/out of central storage when accessed. 这意味着在访问时,变量的值将被复制到中央存储中。 However, there are subtle differences between volatile and the synchronized keyword in Java. 但是,Java中的volatilesynchronized关键字之间存在细微差别。 For example, there is no locking happening with volatile so if multiple threads are updating a volatile variable, race conditions will exist around non-atomic operations. 例如, volatile不会发生锁定,因此如果多个线程正在更新volatile变量,则非原子操作周围将存在竞争条件。 That's why we use AtomicInteger and friends which take care of increment functions appropriately without synchronization. 这就是为什么我们使用AtomicInteger和朋友,它们在没有同步的情况下适当地处理增量函数。

Here's some good reading on the subject: 这里有一些关于这个主题的好读物:

Hope this helps. 希望这可以帮助。

An educated guess at what you're seeing - when not marked as volatile the JIT compiler is using the x86 inc/dec operations which can update the variable atomically. 对你所看到的内容进行了有根据的猜测 - 当没有标记为volatile时,JIT编译器正在使用x86 inc / dec操作,它可以自动更新变量。 Once marked volatile these operations are no longer used and the variable is instead read, incremented/decremented, and then finally written causing more "errors". 一旦标记为volatile,就不再使用这些操作,而是读取变量,递增/递减,然后最终写入导致更多“错误”。

The non-volatile setup has no guarantees it'll function well though - on a different architecture it could be worse than when marked volatile. 非易失性设置无法保证它能够很好地运行 - 在不同的架构上,它可能比标记为volatile时更糟。 Marking the field volatile does not begin to solve any of the race issues present here. 将该区域标记为易失性并未开始解决此处出现的任何种族问题。

One solution would be to use the AtomicInteger class, which does allow atomic increments/decrements. 一种解决方案是使用AtomicInteger类,它允许原子增量/减量。

Volatile variables act as if each interaction is enclosed in a synchronized block. 易失性变量就像每个交互都包含在同步块中一样。 As you mentioned, increment and decrement is not atomic, meaning each increment and decrement contains two synchronized regions (the read and the write). 正如您所提到的,递增和递减不是原子的,这意味着每个递增和递减包含两个同步区域(读取和写入)。 I suspect that the addition of these pseudolocks is increasing the chance that the operations conflict. 我怀疑增加这些伪码会增加操作冲突的可能性。

In general the two threads would have a random offset from another, meaning that the likelihood of either one overwriting the other is even. 通常,两个线程将具有与另一个线程的随机偏移,这意味着任何一个线程覆盖另一个线程的可能性是均匀的。 But the synchronization imposed by volatile may be forcing them to be in inverse-lockstep, which, if they mesh together the wrong way, increases the chance of a missed increment or decrement. 但是由volatile引起的同步可能迫使它们处于反向锁步状态,如果它们以错误的方式啮合在一起,则会增加错过增量或减量的机会。 Further, once they get in this lockstep, the synchronization makes it less likely that they will break out of it, increasing the deviation. 此外,一旦他们进入这个锁步,同步使他们不太可能突破它,增加偏差。

I stumbled upon this question and after playing with the code for a little bit found a very simple answer. 我偶然发现了这个问题,在玩了一段代码后发现了一个非常简单的答案。

After initial warm up and optimizations (the first 2 numbers before the zeros) when the JVM is working at full speed T1 simply starts and finishes before T2 even starts, so count is going all the way up to 10000 and then to 0. When I changed the number of iterations in the worker threads from 10000 to 100000000 the output is very unstable and different every time. 在初始预热和优化(零前的前两个数字)之后,当JVM全速工作时, T1只是 T2开始之前启动并完成 ,因此count一直上升到10000然后再到0.当我将工作线程中的迭代次数从10000更改为100000000,输出非常不稳定,每次都不同。

The reason for the unstable output when adding volatile is that it makes the code much slower and even with 10000 iterations T2 has enough time to start and interfere with T1 . 添加volatile时输出不稳定的原因是它使代码慢得多,即使有10000次迭代, T2也有足够的时间启动并干扰T1

The reason for all those zeroes is not that the ++'s and --'s are balancing each other out. 所有这些零的原因并不是 ++和 - 是相互平衡的。 The reason is that there is nothing here to cause count in the looping threads to affect count in the main thread. 其原因是,这里没有什么导致count的循环线程影响count在主线程。 You need synch blocks or a volatile count (a "memory barrier) to force the JVM to make everything see the same value. With your particular JVM/hardware, what is most likely happening that the value is kept in a register at all times and never getting to cache--let alone main memory--at all. 您需要同步块或易失性count (“内存屏障”)来强制JVM使所有内容看到相同的值。对于您的特定JVM /硬件,最有可能发生的是,值始终保存在寄存器中,从来没有得到缓存 - 更不用说主存了 - 根本没有。

In the second case you are doing what you intended: non-atomic increments and decrements on the same course and getting results something like what you expected. 在第二种情况下,你正在按照你的意图行事:同一course非原子增量和减量,并得到与预期相似的结果。

This is an ancient question, but something needed to be said about each thread keeping it's own , independent copy of the data. 这是一个古老的问题,但需要说明每个线程保留它自己的独立数据副本。

如果您看到count的值不是 10000的倍数,则只会显示您的优化程序较差。

It doesn't 'make things less synchronized'. 它不会“减少同步”。 It makes them more synchronized, in that threads will always 'see' an up to date value for the variable. 它使它们更加同步,因为线程总是“看到”变量的最新值。 This requires erection of memory barriers, which have a time cost. 这需要建立具有时间成本的记忆障碍。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM