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

rus*_*ell 4 java multithreading 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...
}
Run Code Online (Sandbox Code Playgroud)

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

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

但这是对的吗?或者我的代码有一些属性(可能是它只是一个原始变量等),我实际上可以信任它来消除对volatile关键字的需求?

Gra*_*ray 5

即使我没有在原始实例变量i上使用volatile.我预计,如果我没有让我变得不稳定,那么我会遇到很多可见性问题

通过创建getAndIncrement()get()方法synchronized,所有正在修改的线程i都正确地锁定它以进行更新和值的检索.这些synchronized块使得它不必ivolatile因为它们也确保了存储器同步.

也就是说,你应该使用一个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();
}
Run Code Online (Sandbox Code Playgroud)

我会得到很多可见性问题,例如,线程1将i从0递增到1,但是线程2仍然看到值0并且还将其递增到1,导致最终值小于正确值.

如果您的get()方法不是,synchronized那么您的增量可能会被正确处理,但其他线程将无法i正确查看已发布的值.但是这两种方法都synchronized可以确保读写时的内存同步. synchronized也做锁,这样你就可以做到i++.再次AtomicInteger处理内存同步和增量竞争条件更有效.

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