繁体   English   中英

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

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

所以我刚刚学习了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

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

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

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

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

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

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

希望这可以帮助。

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

非易失性设置无法保证它能够很好地运行 - 在不同的架构上,它可能比标记为volatile时更糟。 将该区域标记为易失性并未开始解决此处出现的任何种族问题。

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

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

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

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

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

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

所有这些零的原因并不是 ++和 - 是相互平衡的。 其原因是,这里没有什么导致count的循环线程影响count在主线程。 您需要同步块或易失性count (“内存屏障”)来强制JVM使所有内容看到相同的值。对于您的特定JVM /硬件,最有可能发生的是,值始终保存在寄存器中,从来没有得到缓存 - 更不用说主存了 - 根本没有。

在第二种情况下,你正在按照你的意图行事:同一course非原子增量和减量,并得到与预期相似的结果。

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

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

它不会“减少同步”。 它使它们更加同步,因为线程总是“看到”变量的最新值。 这需要建立具有时间成本的记忆障碍。

暂无
暂无

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

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