是否有必要使原始实例变量可变?

Is it necessary to make a primitive instance variable volatile?

为了试验多线程概念,我正在实现我自己的使用悲观锁定的 AtomicInteger 版本。它看起来像这样:

public class ThreadSafeInt {
    public int i; // Should this be volatile?

    public ThreadSafeInt(int i) {
        this.i = i;
    }

    public synchronized int get() {
        return i;
    }

    public synchronized int getAndIncrement() {
        return this.i++;
    }
    // other synchronized methods for incrementAndGet(), etc...
}

我编写了一个测试,它采用 ThreadSafeInt 的一个实例,将其提供给数百个线程,并使每个线程调用 getAndIncrement 100,000 次。我看到的是所有增量都正确发生,整数的值正好是 (number of threads) * (number of increments per thread),即使我没有在原始实例变量 i 上使用 volatile。我预计如果我没有使 i 易变,那么我会遇到很多可见性问题,例如,线程 1 将 i 从 0 递增到 1,但线程 2 仍然看到值 0并将其递增到仅 1,导致最终值小于正确值。

我知道可见性问题是随机发生的,并且可能取决于我的环境属性,因此即使存在潜在的可见性问题,我的测试看起来也能正常工作。所以我倾向于认为volatile关键字还是有必要的。

但这是正确的吗?或者我的代码中是否有一些 属性(也许它只是一个原始变量等),我实际上可以相信它可以避免对 volatile 关键字的需要?

even though I'm not using volatile on the primitive instance variable i. I expected that if I did not make i volatile, then I would get lots of visibility problems

通过使您的 getAndIncrement()get() 方法 synchronized,所有正在修改 i 的线程都正确锁定它以进行更新和检索价值。 synchronized 块使 i 不必成为 volatile 因为它们还确保内存同步。

也就是说,您应该使用 AtomicInteger 来代替 volatile int 字段。 AtomicInteger getAndIncrement() 方法无需求助于 synchronized 块即可更新值,该块速度更快,同时仍然是线程安全的。

public final AtomicInteger i = new AtomicInteger();
...
// no need for synchronized here
public int get() {
    return i.get();
}
// nor here
public int getAndIncrement() {
    return i.getAndIncrement();
}

I would get lots of visibility problems where, for instance, thread 1 increments i from 0 to 1, but thread 2 still sees the value of 0 and also increments it to only 1, causing a final value that is less than the correct value.

如果您的 get() 方法不是 synchronized,那么您的增量可能会得到正确处理,但其他线程不会看到 i 的值被正确发布。但是这两种方法都是 synchronized 这确保了读写时的内存同步。 synchronized 也进行锁定,以便您可以进行 i++。同样,AtomicInteger 更有效地处理内存同步和增量竞争条件。

更具体地说,当输入 synchronized 块时,它会跨越读取内存屏障,这与从 volatile 字段读取相同。当退出 synchronized 块时,它会跨越写入内存屏障,这与写入 volatile 字段相同。 synchronized 块的不同之处在于,还有锁定以确保一次只有一个人锁定特定对象。