[英]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中的volatile
和synchronized
关键字之间存在细微差别。 例如, 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.