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

When I use volatile in project, why the code below shows different result?

第一个:

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,为什么?

考虑一下您在 increase 方法中执行的这个操作。您首先读取现有值,然后将其递增并写回。这里有几个指令,可以被中断。获得小于 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();
}

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

第二个测试的结果2000不是jls保证的,你可以让线程在increment之前的某个时候休眠,这样更容易"break":

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

    }
    inc++;
}

您可能会得到:

1997
1999

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

volatile可以保证对一个变量的改变总是对其他线程可见,但不能保证对这个变量的操作是原子的。

假设i = 1,thread1和thread2可能同时读到1,然后递增到2,然后再回写,结果是错误的。

这只是巧合。这两个变体同样被破坏,但第二个定义了两个不同的 classes 做同样的事情,所以在启动第二个线程之前,这个额外的 class 必须被加载、验证和初始化。这种开销使第一个线程抢先一步,提高了在第二个甚至开始之前完全完成的机会。

所以竞争条件并没有实现,但是由于不能保证这次执行,它仍然是一个包含数据竞争可能性的错误程序。 运行 在具有更快 class loading/initialization 或 ahead-of-time 策略的环境中,同一程序可能会表现出与第一个变体相同的行为。

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