我最近遇到过volatile关键字这种奇怪的行为。据我所知,
将volatile关键字应用于变量以反映对数据所做的更改 变量由一个线程到另一个线程上。
volatile关键字阻止在线程上缓存数据。
我做了一个小测试........
我使用了一个名为count的整数变量,并在其上使用了volatile关键字。
然后做了2个不同的线程将变量值增加到10000,所以最终结果应该是20000。
但事实并非如此,使用volatile关键字我一直没有得到20000,而是18534,15000等......有时候是20000。
< / LI>但是虽然我使用了synchronized关键字,但它工作得很好,为什么...... ??
任何人都可以向我解释volatile关键字的这种行为。
我发布的代码包含volatile关键字以及带有synchronzied关键字的代码。
以下代码与变量计数上的volatile关键字行为不一致
public class SynVsVol implements Runnable{
volatile int count = 0;
public void go(){
for (int i=0 ; i<10000 ; i++){
count = count + 1;
}
}
@Override
public void run() {
go();
}
public static void main(String[] args){
SynVsVol s = new SynVsVol();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Total Count Value: "+s.count);
}
}
以下代码与方法go()上的synchronized关键字完美匹配。
public class SynVsVol implements Runnable{
int count = 0;
public synchronized void go(){
for (int i=0 ; i<10000 ; i++){
count = count + 1;
}
}
@Override
public void run() {
go();
}
public static void main(String[] args){
SynVsVol s = new SynVsVol();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Total Count Value: "+s.count);
}
}
答案 0 :(得分:12)
count = count + 1
不是原子的。它有三个步骤:
这三个步骤交织在一起,导致执行路径不同,导致值不正确。如果您想避免使用synchronized关键字,请使用AtomicInteger.incrementAndGet()
。
尽管volatile关键字的行为与您所描述的非常相似,但它仅适用于每个单独的操作,而不适用于所有三个操作。
答案 1 :(得分:7)
volatile
关键字不是同步原语。它只是防止在线程上缓存值,但它不会阻止两个线程修改相同的值并同时将其写回。
假设有两个线程需要递增计数器,现在设置为5.两个线程都看到5,从中取出6,并将其写回计数器。如果计数器不是volatile
,则两个线程都可以假设它们知道值为6,并跳过下一个读取。然而,它是不稳定的,所以它们都会读回6,并继续递增。由于线程没有进入锁定步骤,您可能会在输出中看到与10000不同的值,但实际上您几乎没有机会看到20000。
答案 2 :(得分:4)
变量volatile
这一事实并不意味着它所涉及的每个操作都是原子的。例如,SynVsVol.Go
中的这一行:
count = count + 1;
将首先读取count
,然后递增,然后将结果写回结果。如果某个其他线程同时执行它,则结果取决于命令的交错。
现在,当您添加syncronized
时,SynVsVol.Go
会以原子方式执行。也就是说,增量是由一个线程整体完成的,另一个不能修改count
直到完成。
最后,缓存仅在同步块中修改的成员变量要容易得多。获取监视器时,编译器可以读取它们的值,将其缓存在寄存器中,对该寄存器进行所有更改,并在释放监视器时最终将其刷回主存储器。当您在同步块中调用wait
时,以及当您使用其他线程notify
时,也会出现这种情况:缓存的成员变量将被同步,并且您的程序将保持一致。 That's guaranteed即使成员变量未声明为volatile:
同步确保线程在之前或之前写入内存 在同步块期间以可预测的方式可见 到同一监视器上同步的其他线程。
答案 3 :(得分:3)
您的代码被破坏了,因为它将volatile
上的读取和递增操作视为原子,而不是。代码不包含数据竞争,但它在int
上包含竞争条件。