【问题标题】:Is volatile not needed for objects' members but only on primitive members?对象的成员不需要 volatile ,而只需要原始成员吗?
【发布时间】:2017-10-17 17:46:09
【问题描述】:

我的代码是

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

这表明当第一个线程将成员变量分配给新对象时,其他线程也是可见的。即使“mbq”没有被声明为 volatile。

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

作为 object 的类成员不需要 volatile 吗?它们总是与主存储器同步吗?只有原始成员变量(int、long、boolean 等?)需要 Volatile

【问题讨论】:

  • 您希望代码在您尝试时碰巧可以工作,还是按标准保证可以工作的代码?
  • "保证":)
  • @shmosel 抱歉。复制粘贴错误。添加了缺失的行。

标签: java multithreading volatile


【解决方案1】:

对于引用和原语一样需要。您的输出没有显示可见性问题这一事实并证明不存在。一般来说,很难证明不存在并发错误。但这里有一个简单的反证说明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();
    }
}

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

【讨论】:

  • 此示例是否需要多个内核/处理器?
  • @BasilBourque 我只在我自己的机器上试过,所以我不能肯定。它不适合你吗?
  • 我的意思是一般。我想知道您的示例代码所展示的可见性问题在单核机器上是否不是问题?或者线程可能会因为 CPU 缓存而在单个内核上看到不同的可见性?
【解决方案2】:

一般来说,您此时没有看到某事发生,并不意味着以后不会发生。 尤其是适用于并发代码。您可以使用 jcstress 库,并尝试向您展示您的代码可能有什么问题。

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

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

【讨论】:

    【解决方案3】:

    您的代码不是对 volatile 的有用测试。它可以在有或没有 volatile 的情况下工作,不是偶然的,而是根据规范。

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

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

    线程上的 start() 调用发生在已启动线程中的任何操作之前。

    线程中的所有操作都发生在任何其他线程从该线程上的 join() 成功返回之前。

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

    但不,说引用不需要 volatile 是不准确的。 volatile 有一个happens-before关系:

    对 volatile 字段(第 8.3.1.4 节)的写入发生在对该字段的每次后续读取之前。

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

    【讨论】:

    • 更改发生在线程执行中,即在两个线程都启动之后。所以我不明白你强调的重点。
    猜你喜欢
    • 1970-01-01
    • 2016-02-05
    • 1970-01-01
    • 2012-06-22
    • 1970-01-01
    • 2011-10-22
    • 1970-01-01
    • 1970-01-01
    • 2015-10-18
    相关资源
    最近更新 更多