【问题标题】:Is it necessary to make a primitive instance variable volatile?是否有必要使原始实例变量易失?
【发布时间】:2025-12-04 09:40:01
【问题描述】:

只是为了试验多线程概念,我正在实现我自己的 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 volatile,那么我会遇到很多可见性问题,例如,线程1 将i 从0 增加到1,但线程2 仍然看到0 的值并且还会增加它仅1,导致最终值小于正确值。

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

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

【问题讨论】:

    标签: java multithreading volatile


    【解决方案1】:

    即使我没有在原始实例变量 i 上使用 volatile。我预计如果我不让 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,导致最终值小于正确的值。

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

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

    【讨论】:

    • 你是说 synchronized 不仅保证代码路径一次只能由一个线程执行,而且还保证在同步块内部触及的任何变量都被视为它是易变的?我认为即使您使用同步,两个线程也有可能在变量中观察到不同的值 - 所以我认为您的意思是事实并非如此。
    • @russell 这真正的意思是i++,它通常不是原子操作,因为synchronized而变成原子操作。
    • "@russel 当写入和读取方法同步时,如在您的代码中,普通字段变量被视为易失性。学习“Java 内存模型”以开发有意义的测试程序。
    • 我在我的问题@russell 的末尾添加了一些内容来解释内存障碍以及volatilesynchronized 之间的相似之处(和不同之处)。
    最近更新 更多