Java - 对可变对象的易失性引用 - 对象的字段的更新对所有线程都是可见的

时间:2014-05-25 13:16:17

标签: java multithreading volatile java-memory-model

...没有额外的同步?下面的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易变?
  • 将根更改为root = new Node()一样简单(只有编写器线程会更改根,除了调用Tree构造函数的主线程外)

相关:

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

2 个答案:

答案 0 :(得分:10)

没有

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

从volatile字段加载对象的引用后,您有一个与任何其他对象没有区别的对象,并且波动性没有进一步的影响。

答案 1 :(得分:4)

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

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

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

关于第一个问题。您可以使用 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 ,可以消除数据竞争。如果没有数据竞赛,则可见性的更新没有问题。