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

时间:2010-11-09 04:53:53

标签: java concurrency synchronization thread-safety volatile

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

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

我已经阅读了When exactly do you use the volatile keyword in Java?所以我觉得我对关键字的作用有了基本的了解(在不同的线程中保持变量的缓存副本的同步,但不是read-update-write safe) 。我知道这段代码当然不是线程安全的。对我的学生来说,作为一个例子,特别不是线程安全的。但是,我很好奇为什么添加volatile关键字会使输出不像关键字不存在时那样“稳定”。

7 个答案:

答案 0 :(得分:6)

  

为什么标记Java变量volatile会使事情变得不那么同步?

使用volatile关键字“为什么代码运行更糟”的问题不是一个有效的问题。由于用于volatile字段的内存模型不同,它的行为与不同。不能依赖于程序的输出在没有关键字的情况下趋于0的事实,如果您转移到具有不同CPU线程或CPU数量的不同架构,则结果差别不大。

此外,重要的是要记住虽然x++似乎是原子的,但它实际上是一个读/修改/写操作。如果在许多不同的体系结构上运行测试程序,您会发现不同的结果,因为JVM实现volatile的方式与硬件有关。访问volatile字段也可能比访问缓存字段慢得多 - 有时会增加1或2个数量级,这将改变程序的时间。

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

以下是关于这个主题的一些好读物:

希望这有帮助。

答案 1 :(得分:5)

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

非易失性设置无法保证它能够正常运行 - 在不同的架构上,它可能比标记为volatile时更糟糕。将该字段标记为易失性并不能解决此处出现的任何种族问题。

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

答案 2 :(得分:3)

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

通常,两个线程将具有与另一个线程的随机偏移,这意味着任何一个线程覆盖另一个线程的可能性是均匀的。但是由volatile引起的同步可能迫使它们处于反向锁步状态,如果它们以错误的方式啮合在一起,则会增加错过增量或减量的机会。此外,一旦他们进入这个锁步,同步使他们不太可能突破它,增加偏差。

答案 3 :(得分:1)

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

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

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

答案 4 :(得分:0)

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

答案 5 :(得分:0)

所有这些零的原因是而不是,而++和 - 是相互平衡的。原因是这里没有任何内容导致循环线程中的count影响主线程中的count。您需要同步块或易失性count(“内存屏障”)来强制JVM使所有内容看到相同的值。对于您的特定JVM /硬件,最有可能发生的是将值保存在寄存器中在任何时候都永远不会缓存 - 更不用说主存储器了。

在第二种情况下,你正在做你想要的事情:同一course上的非原子增量和减量,并得到与预期相似的结果。

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

答案 6 :(得分:-1)

它不会“减少同步”。这使得它们更多同步,因为线程将始终“看到”变量的最新值。这需要建立记忆障碍,这需要花费时间。