【问题标题】:Java - volatile reference to mutable object - will updates to the object's fields be visible to all threadsJava - 对可变对象的易失性引用 - 对对象字段的更新将对所有线程可见
【发布时间】:2014-05-25 13:16:16
【问题描述】:

...没有额外的同步?下面的 Tree 类旨在被多个线程访问(它是一个单例,但不是通过枚举实现的)

class Tree {

    private volatile Node root;

    Tree() {
        root = new Node();
        // the threads are spawned _after_ the tree is constructed
    }

    private final class Node {
        short numOfKeys;
    }
}
  • numOfKeys 字段的更新是否会在没有任何显式同步的情况下对读取器线程可见(请注意,读取器和写入器都必须获取 ReentrantReadWriteLock 的实例 - 每个节点的相同实例 - 但除非这样做)?如果不是,让numOfKeys volatile 就足够了吗?
  • 更改根就像root = new Node() 一样简单(只有编写器线程会更改根,除了调用 Tree 构造函数的主线程)

相关:

编辑:对 Java 5 后的语义感兴趣

【问题讨论】:

  • 我不明白ReentrantReadWriteLock 的部分。有 ReadWriteLock 吗?或者问题是没有 ReadWriteLock 是否可能?
  • @nosid:编辑了这个问题——我提到了锁,因为它的存在是为了协调读者/作者,它可能会有所作为——但问题不在于锁只是访问时必要的同步可变对象的字段
  • 您可能希望将该内部类设为静态以提高内存效率。
  • @DavidEhrmann:这是非静态内部类的罕见用途之一(至少在我的设计中) - 节点必须知道树(为了记录,它是基于磁盘的 b+ 树)跨度>

标签: java multithreading volatile java-memory-model


【解决方案1】:

没有。

volatile 字段中放置对对象的引用不会以任何方式影响对象本身。

一旦您从 volatile 字段加载对对象的引用,您就拥有了一个与任何其他对象没有什么不同的对象,并且波动性没有进一步的影响。

【讨论】:

  • 嗯,对 volatile 字段的读写确实建立了发生在关系之前。还有问题的其他部分呢?
  • 所以这意味着标记对对象volatile的引用仅确保每个线程都获得正确的对象,但是当线程尝试读取该对象的属性时,它们仍然可能获得陈旧的值因为该对象的属性不是volatile。对吗?
  • 让事情变得更糟;)如果引用的对象具有易失性和非易失性字段会发生什么。该对象的易失性字段是否获得更新的值而非易失性字段不会。所以,这会使对象状态不一致:|
【解决方案2】:

这是两个问题。让我们从第二个开始。

将新构造的对象分配给 volatile 变量效果很好。每个读取 volatile 变量的线程都会看到一个 完全构造的 对象。不需要进一步同步。这种模式通常与不可变类型结合使用。

class Tree {
    private volatile Node node;
    public void update() {
        node = new Node(...);
    }
    public Node get() {
        return node;
    }
}

关于第一个问题。您可以使用 volatile 变量来同步访问 non-volatile 变量。以下清单显示了一个示例。想象一下,这两个变量如图所示被初始化,并且这两个方法同时执行。可以保证,如果第二个线程看到foo 的更新,它也会看到bar 的更新。

volatile int foo = 0;
int bar = 0;

void thread1() {
    bar = 1;
    foo = 1; // write to volatile variable
}

void thread2() {
    if (foo == 1) { // read from volatile variable
        int r = bar; // r == 1
    }
}

但是,您的示例有所不同。读取和写入可能如下所示。与上面的示例相反,两个线程都从 volatile 变量中读取。但是,volatile 变量上的读取操作不会相互同步。

void thread1() {
    Node temp = root; // read from volatile variable
    temp.numOfKeys = 1;
}

void thread2() {
    Node temp = root; // read from volatile variable
    int r = temp.numOfKeys;
}

换句话说:如果线程 A 写入 volatile 变量 x 并且线程 B 读取值写入x,然后在读操作之后,线程B将看到线程A的所有写操作,发生在写之前>x。但是如果没有对 volatile 变量的写操作,对其他变量的更新没有影响。


这听起来比实际上要复杂。其实只有一条规则需要考虑,你可以在JLS8 §17.4.5找到:

[..] 如果所有顺序一致的执行都没有数据竞争,[..] 那么程序的所有执行都将看起来是顺序一致的。

简单地说,如果两个线程可以同时访问同一个变量,并且至少一个操作是写操作,并且该变量是非易失性,则存在数据竞争 /em>。 数据竞争可以通过将共享变量声明为volatile来消除。没有数据竞争,更新的可见性没有问题。

【讨论】:

  • 谢谢 - 那么让 numOfKeys volatile 就足够了吗?
  • @Mr_and_Mrs_D: volatile 解决了这个问题。我在答案中添加了一段关于内存模型的最重要规则。
猜你喜欢
  • 2012-03-11
  • 1970-01-01
  • 1970-01-01
  • 2016-12-23
  • 2021-02-27
  • 1970-01-01
  • 2011-02-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多