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

...没有额外的同步?下面的 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;
    }
}
  • 在没有任何显式同步的情况下,对字段的更新是否对读取器线程可见(请注意,读取器和写入器都必须获取 ReentrantReadWriteLock 的实例 - 每个节点的相同实例 - 但除此之外)?如果不是,那么挥发性就足够了吗?numOfKeysnumOfKeys
  • 更改根目录是否简单到(除了调用 Tree 构造函数的主线程之外,只有编写器线程会更改根目录)root = new Node()

相关:

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


答案 1

不。

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

一旦从易失性字段加载对对象的引用,您就拥有了一个与任何其他对象无异的对象,并且波动性没有进一步的影响。


答案 2

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

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

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

关于第一个问题。您可以使用易失性变量来同步对非易失性变量的访问。以下清单显示了一个示例。假设两个变量初始化,如图所示,并且两个方法同时执行。可以保证,如果第二个线程看到 的更新,它也将看到 到 的更新。foobar

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
    }
}

但是,您的示例是不同的。阅读和写作可能如下所示。与上面的示例相反,两个线程都从易失性变量中读取。但是,对易失性变量的读取操作不会彼此同步。

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 写入易失性变量 x,并且线程 B 读取写入 x 的值,则在读取操作之后,线程 B 将看到线程 A 的所有写入操作,这些操作发生在写入 x 之前。但是,如果没有对易失性变量的写入操作,则不会对其他变量的更新产生影响。


这听起来比实际情况更复杂。实际上,只有一个规则需要考虑,您可以在JLS8 §17.4.5中找到:

[..]如果所有顺序一致的执行都没有数据争用,则 [..] 则程序的所有执行都将显示为顺序一致。

简而言之,如果两个线程可以同时访问同一个变量,则存在数据争用,至少有一个操作是写入操作,并且该变量是非易失性的。通过将共享变量声明为易失性,可以消除数据争用。如果没有数据竞跑,更新的可见性就没有问题。