对象的成员不需要 volatile 而只需要原始成员吗?

Is volatile not needed for objects' members but only on primitive members?

我的代码是

package threadrelated;
import threadrelated.lockrelated.MyNonBlockingQueue;

public class VolatileTester extends Thread {

 MyNonBlockingQueue mbq ;

 public static void main(String[] args) throws InterruptedException {

    VolatileTester vt = new VolatileTester();
    vt.mbq = new MyNonBlockingQueue(10);
    System.out.println(Thread.currentThread().getName()+" "+vt.mbq);
    Thread t1 = new Thread(vt,"First");
    Thread t2 = new Thread(vt,"Secondz");
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(Thread.currentThread().getName()+" "+vt.mbq);

}
@Override
public void run() {
    System.out.println(Thread.currentThread().getName()+" before    "+mbq);
    mbq = new MyNonBlockingQueue(20);
    try {
        Thread.sleep(TimeUnit.SECONDS.toMillis(10));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName()+" after   "+mbq);
}

}

输出是

main threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz before    threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
First before    threadrelated.lockrelated.MyNonBlockingQueue@72fcb1f4
Secondz after   threadrelated.lockrelated.MyNonBlockingQueue@7100650c
First after   threadrelated.lockrelated.MyNonBlockingQueue@7100650c
main threadrelated.lockrelated.MyNonBlockingQueue@7100650c

说明当First线程将成员变量赋值给new对象时,同样对其他线程可见。即使 "mbq" 没有被声明为 volatile.

我使用断点来尝试不同的操作顺序。但我的观察是一个线程可以立即看到另一个线程的影响。

class 对象成员不需要 volatile 吗?它们总是与主存同步吗?仅原始成员变量(int、long、boolean 等?)

才需要 Volatile

它对于引用和基元一样都是必需的。您的输出没有显示可见性问题这一事实并不能证明不存在。一般来说,很难证明不存在并发错误。但这里有一个简单的反证明,显示 volatile:

的必要性
public class Test {
    static volatile Object ref;

    public static void main(String[] args) {
        // spin until ref is updated
        new Thread(() -> {
            while (ref == null);
            System.out.println("done");
        }).start();

        // wait a second, then update ref
        new Thread(() -> {
            try { Thread.sleep(1000); } catch (Exception e) {}
            ref = new Object();
        }).start();
    }
}

该程序运行一秒钟,然后打印 "done"。删除 volatile 并且它不会终止,因为第一个线程永远不会看到更新的 ref 值。 (免责声明:与任何并发测试一样,结果可能会有所不同。)

一般来说,您此时没有看到某事发生,并不意味着以后不会发生。 特别是 并发代码。您可以使用 jcstress 库,它会尝试向您展示您的代码可能存在的问题。

volatile 变量不同于其他变量,因为它在 CPU 级别引入了 memory barries。没有这些,就无法保证 whenwhat 线程看到来自另一个线程的更新。用简单的话来说,这些被称为 StoreLoad|StoreStore|LoadLoad|LoadStore.

所以使用 volatile 保证了可见性效果,实际上它是你唯一可以依赖的可见性效果(除了使用 Unsafe 和 locks/synchronized 关键字)。您还必须考虑到您正在针对 一个特定的 CPU 进行测试,很可能是 x86。但是对于不同的 CPU(比如说 ARM),事情会崩溃得更快。

您的代码不是对 volatile 的有用测试。无论是否使用 volatile,它都可以正常工作,这不是偶然的,而是根据规范。

包含的代码可以更好地测试 volatile 关键字,因为该字段是否可变会产生影响。如果您采用该代码,使该字段成为非易失性的,并在循环中插入一个 println,那么您应该会看到从另一个线程设置的该字段的值是可见的。这是因为 println 在打印流上同步,插入内存屏障。

在您的示例中还有另外两件事插入了这些障碍,导致更新跨线程可见。 Java Language Specification 列出了这些先行关系:

A call to start() on a thread happens-before any actions in the started thread.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

这意味着您发布的代码中不需要 volatile。新启动的线程可以看到从 main 传入的队列,一旦线程完成,main 就可以看到对队列的引用。在线程启动时间和 println 执行时间之间有一个 window,字段的内容可能是陈旧的,但代码中没有任何内容正在测试它。

但是不,说引用不需要 volatile 是不准确的。 volatile 存在先行关系:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

规范不区分包含引用的字段和包含基元的字段,该规则适用于两者。这又回到了 Java 是按值调用,引用是值。