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

时间:2017-03-25 16:12:57

标签: 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...
}

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

我理解可见性问题是随机发生的,并且可能取决于我的环境的属性,因此即使可见性问题存在固有的可能性,我的测试也可能看起来很好。所以我倾向于认为volatile关键字仍然是必要的。

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

1 个答案:

答案 0 :(得分:5)

  

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

通过创建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();
}
  

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

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

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