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

发布于 2024-10-01 06:03:25 字数 1696 浏览 2 评论 0原文

所以我刚刚在为明天助教的部分编写一些示例时了解了 volatile 关键字。我编写了一个快速程序来证明 ++ 和 -- 操作不是原子的。

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

        }
    }
}

正如预期的那样,该程序的输出通常如下:

-1521
  -39
    0
    0
    0
    0
    0
    0

但是,当我更改:

private static int count = 0;

我的输出更改为

private static volatile int count = 0;

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

我已阅读 你到底什么时候在 Java 中使用 volatile 关键字? 所以我觉得我已经基本了解了该关键字的作用(在不同线程中的变量的缓存副本之间保持同步,但读-更新-写不安全)。我知道这段代码当然不是线程安全的。作为我的学生的例子,它特别不是线程安全的。但是,我很好奇为什么添加 volatile 关键字会使输出不像关键字不存在时那样“稳定”。

So I just learned about the volatile keyword while writing some examples for a section that I am TAing tomorrow. 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? 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). 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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

听风吹 2024-10-08 06:03:25

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

使用 volatile 关键字“为什么代码运行得更糟”的问题不是一个有效的问题。由于用于易失性字段的内存模型不同,它的行为不同。在没有关键字的情况下,程序的输出趋向于 0 的事实是不能可靠的,如果您迁移到具有不同 CPU 线程或 CPU 数量的不同体系结构,则截然不同的结果将很常见。

另外,重要的是要记住,虽然 x++ 看起来是原子的,但它实际上是读/修改/写操作。如果您在多种不同的体系结构上运行测试程序,您会发现不同的结果,因为 JVM 如何实现 易失性 非常依赖于硬件。访问易失性字段也可能比访问缓存字段慢得多——有时慢 1 或 2 个数量级,这会改变程序的时间。

使用 volatile 关键字确实为特定字段建立了内存屏障,并且(从 Java 5 开始)此内存屏障扩展到所有其他共享变量。这意味着在访问时变量的值将被复制进/出中央存储。然而,Java 中的 volatilesynchronized 关键字之间存在细微差别。例如,易失性不会发生锁定,因此如果多个线程正在更新易失性变量,则非原子操作周围将存在竞争条件。这就是为什么我们使用 AtomicInteger 以及在不同步的情况下适当处理增量函数的朋友。

这里有一些关于这个主题的好读物:

希望这会有所帮助。

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

The question "why does the code run worse" with the volatile keyword is not a valid question. 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.

Also, it is important to remember that although x++ seems atomic, it is actually a read/modify/write operation. 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. 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.

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. 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. 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. That's why we use AtomicInteger and friends which take care of increment functions appropriately without synchronization.

Here's some good reading on the subject:

Hope this helps.

相思碎 2024-10-08 06:03:25

对您所看到的内容进行有根据的猜测 - 当未标记为易失性时,JIT 编译器正在使用 x86 inc/dec 操作,该操作可以自动更新变量。一旦标记为易失性,这些操作将不再使用,而是读取变量,递增/递减,然后最终写入,从而导致更多“错误”。

非易失性设置并不能保证它能够正常运行 - 在不同的架构上,它可能比标记为易失性时更糟糕。将字段标记为不稳定并不能解决这里存在的任何竞争问题。

一种解决方案是使用 AtomicInteger 类,它允许原子递增/递减。

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. Once marked volatile these operations are no longer used and the variable is instead read, incremented/decremented, and then finally written causing more "errors".

The non-volatile setup has no guarantees it'll function well though - on a different architecture it could be worse than when marked 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.

泅渡 2024-10-08 06:03:25

易失性变量的作用就好像每个交互都包含在一个同步块中。正如您所提到的,增量和减量不是原子的,这意味着每个增量和减量都包含两个同步区域(读取和写入)。我怀疑添加这些伪锁会增加操作冲突的可能性。

一般来说,两个线程与另一个线程之间会有随机偏移,这意味着其中一个线程覆盖另一个线程的可能性是偶数。但是,易失性强加的同步可能会迫使它们处于反锁步状态,如果它们以错误的方式啮合在一起,就会增加错过增量或减量的机会。此外,一旦他们进入这种同步状态,同步就会使他们不太可能打破这种状态,从而增加偏差。

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. Further, once they get in this lockstep, the synchronization makes it less likely that they will break out of it, increasing the deviation.

风和你 2024-10-08 06:03:25

我偶然发现了这个问题,在玩了一下代码后发现了一个非常简单的答案。

在初始预热和优化之后(零之前的前 2 个数字),当 JVM 全速工作时,T1 只是之前开始和完成 T2 甚至开始,所以 count 会一直增加到 10000,然后到 0。
当我将工作线程中的迭代次数从 10000 更改为 100000000 时,输出非常不稳定并且每次都不同。

添加 volatile 时输出不稳定的原因是,它使代码变得更慢,即使迭代 10000 次,T2 也有足够的时间启动并干扰 T1< /代码>。

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.

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.

流云如水 2024-10-08 06:03:25

所有这些零的原因不是 ++ 和 -- 相互平衡。原因是这里没有任何东西可以导致<循环线程中的 code>count 会影响主线程中的 count。您需要同步块或易失性计数(“内存屏障”)来强制 JVM 使所有内容都看到相同的值。对于您的特定 JVM/硬件,最有可能发生的情况是保留该值始终在寄存器中并且永远不会进入缓存(更不用说主内存了)

在第二种情况下,您正在执行您想要的操作:在同一课程上进行非原子递增和递减。并获得与您期望的结果类似的结果。

这是一个古老的问题,但需要说明的是每个线程都保留其自己的独立数据副本。

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

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.

This is an ancient question, but something needed to be said about each thread keeping it's own, independent copy of the data.

那片花海 2024-10-08 06:03:25

如果您看到 count 的值不是 10000 的倍数,则仅表明您的优化器很差。

If you see a value of count that is not a multiple of 10000, it just shows that you have a poor optimiser.

弄潮 2024-10-08 06:03:25

它不会“降低同步性”。它使它们更加同步,因为线程将始终“看到”变量的最新值。这需要建立记忆屏障,这是有时间成本的。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文