当我在项目中使用volatile时,为什么下面的代码显示不同的结果?

时间:2018-03-20 10:22:41

标签: java multithreading volatile

首先:

public class VolatileTest{
    public volatile int inc = 0;
    public void increase(){
        inc++;
    }

    public static void main(String[] args) {
        VolatileTest test = new VolatileTest();
        for(int i = 0 ; i < 2 ; i ++){
            new Thread(){
                public void run(){
                    for(int j = 0 ; j < 1000 ; j++)
                        test.increase();
                }
            }.start();
        } 
        while(Thread.activeCount() > 1)Thread.yield();
        System.out.println(test.inc);
    }
}

第二

public class VolatileTest{
    public volatile int inc = 0;
    public void increase(){
        inc++;
    }

    public static void main(String[] args) {
        VolatileTest test = new VolatileTest();
        new Thread(){
            public void run(){
                for(int j = 0 ; j < 1000 ; j++)
                    test.increase();
            }
        }.start();
        new Thread(){
            public void run(){
                for(int j = 0 ; j < 1000 ; j++)
                    test.increase();
            }
        }.start();
        while(Thread.activeCount() > 1)Thread.yield();
        System.out.println(test.inc);
    }
}

第一个使用for而第二个不使用,这是唯一的区别,但第一个得到的结果小于2000,第二个得到的结果等于2000,为什么?

3 个答案:

答案 0 :(得分:3)

考虑您在increase方法中执行的此操作。您首先读取现有值,然后递增并将其写回。这里有几条指令,可以中断。获得值的原因是&lt; 2000年是因为竞争条件。使用关键字volatile并不能保证原子性。为了保证原子性,你必须使用锁。试试这个。

private final Object lock = new Object();

public void increase() {
    synchronized (lock) {
        inc++;
    }

}

另一种选择是在这里使用AtomicInteger。所以你的代码现在看起来像这样。

public AtomicInteger inc = new AtomicInteger(0);
public void increase() {
    inc.incrementAndGet();
}

这也保证了顾名思义的原子性。

答案 1 :(得分:1)

jls不保证第二次测试的结果2000,你可以让线程在增加之前的某个时间睡眠,以使它更容易“破解”:

public void increase(){
    try {
        Thread.sleep(20);
    } catch (Exception e) {

    }
    inc++;
}

你可能会得到:

1997
1999

或其他一些不可预测的结果。

volatile可以保证对变量的更改始终对其他线程可见,但不能保证对此变量的操作是原子的。

假设i = 1,thread1和thread2可能同时读取1,并将其增加到2,然后回写,这会导致错误的结果。

答案 2 :(得分:1)

这只是巧合。两个变体同样被破坏,但第二个定义了两个不同的类做同样的事情,因此在启动第二个线程之前,必须加载,验证和初始化这个附加类。这个开销使第一个线程成为一个开端,在第二个开始之前完全完成完成的机会。

因此竞争条件没有实现,但由于这种执行得不到保证,它仍然是一个包含数据竞争可能性的破碎程序。在具有更快的类加载/初始化或提前策略的环境中运行相同的程序可能表现出与第一个变体相同的行为。

同样,请注意,不能保证第一个变体会丢失更新。可能仍然会发生启动第二个线程足够慢以允许第一个线程完成而没有数据争用。即使两个线程都运行,系统的线程调度策略也可能会改变遇到丢失更新的可能性。此外,整个循环可以通过1000优化到单个增量,这不会与volatile变量的要求相矛盾,即使当前版本的HotSpot JVM没有这样做。