当数组变量是 volatile 时,我们是否需要同步访问数组?

Do we need to synchronize access to an array when the array variable is volatile?

我有一个 class 包含对数组的可变引用:

private volatile Object[] objects = new Object[100];

现在,我可以保证,只有一个线程(称之为 writer)可以写入数组。例如,

objects[10] = new Object();

所有其他线程将只读取由 writer 线程写入的值。

问题:这样的读写需要同步才能保证内存一致性吗?

我想,是的,我应该。因为如果 JVM 在写入数组时提供某种内存一致性保证,从性能的角度来看就没有用了。但我不确定。在文档中找不到任何有用的信息。

private volatile Object[] objects = new Object[100];

你只objects这样引用volatile。不是关联的数组实例内容。

Question: Do I need to synchronize such reads and writes in order to ensure memory consistency?

是的。

it would not be useful from performance standpoint if JVM provides some kind of memory consistency guarantees when writing to an array

考虑使用像 CopyOnWriteArrayList 这样的集合(或者你自己的数组包装器,在 mutators 和 read 方法中使用一些 Lock 实现)。

Java 平台也有 Vector(因设计有缺陷而过时)和 synchronized List(在许多情况下速度较慢),但我不建议使用它们。

PS: One more good idea from @SashaSalauyou

您可以使用 AtomicReferenceArray:

final AtomicReferenceArray<Object> objects = new AtomicReferenceArray<>(100);

// writer
objects.set(10, new Object());

// reader
Object obj = objects.get(10);

这将确保 read/write 操作的原子更新和 happens-before 一致性,就好像数组的每个项目都是 volatile.

是的,您需要同步对可变数组元素的访问。

其他人已经解决了如何使用 CopyOnWriteArrayListAtomicReferenceArray 的问题,所以我将转向稍微不同的方向。我还建议阅读 JMM 的主要贡献者之一 Jeremy Manson 的 Volatile Arrays in Java

Now, I can gurantee that the only one thread (call it writer) can write to the array as e.g. follows:

是否可以提供单个作者保证与 volatile 关键字没有任何关系。我想你没有想到这一点,但我只是澄清一下,以免其他读者产生错误的印象(我认为这句话可以构成数据竞赛的双关语).

All other threads will only read values written by the writer thread.

是的,但是就像您的直觉正确引导您一样,这仅适用于数组引用的值。 这意味着除非您正在写入对 volatile 变量的数组引用,否则您将不会获得 volatile 读写合同的写入部分。

这意味着要么你想做一些像

objects[i] = newObj;
objects = objects;

这在很多方面都很丑陋和糟糕。 或者你想在你的作者每次更新时发布一个全新的数组,例如

Object[] newObjects = new Object[100];

// populate values in newObjects, make sure that newObjects IS NOT published yet

// publish newObjects through the volatile variable
objects = newObjects;

这不是一个很常见的用例。

请注意,与不提供 volatile-写入语义的设置数组元素不同,获取数组元素(使用 newObj = objects[i];)确实提供 volatile-读取语义,因为你正在取消引用数组:)

Because it would not be useful from performance standpoint if JVM provides some kind of memory consistency guarantess when writing to an array. But I'm not sure about that.

就像你暗示的那样,确保 volatile 语义所需的内存防护将非常昂贵,如果你在混合中添加虚假共享,情况会变得更糟。

Didn't find anything helpful in documentation.

然后您可以安全地假设数组引用的 volatile 语义与非数组引用的 volatile 语义完全相同,考虑到数组 (即使是原始的)仍然是对象。

根据 JLS § 17.4.5 – Happens-before Order:

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

[...]

A write to a volatile field happens-before every subsequent read of that field.

happens-before 关系非常强。这意味着如果线程 A 写入一个 volatile 变量,并且任何线程 B 稍后读取该变量,那么线程 B 保证看到 volatile 变量本身的变化,以及 在设置 volatile 变量之前,线程 A 所做的所有其他更改,包括对任何其他对象的更改,无论它们是否为 volatile.

然而,这还不够!

元素赋值 objects[10] = new Object(); 不是变量 objects 的写入。它只是读取变量以确定它指向的数组,然后写入另一个变量,该变量包含在位于内存中其他地方的数组对象中。没有 happens-before 关系仅通过读取 volatile 变量建立,因此代码不安全。

正如@DimitarDimitrov 指出的那样,您可以通过对 objects 变量进行虚拟写入来解决这个问题。每对操作——写入线程的 objects = objects; 重新分配加上 reader 线程的 foo = objects[x]; 查找——定义了一个更新的 happens-before关系,因此将 "publish" 作者线程对 reader 线程所做的所有最新更改。这可以工作,但它需要纪律,而且不优雅。

但这有一个更微妙的问题:即使 reader 线程看到数组元素的更新值,仍然不能保证它看到它所引用的对象的字段元素正确,因为以下顺序是可能的:

  1. 作者创建了一些对象 foo
  2. 作家集objects[x] = foo;
  3. Reader 检查 objects[x] 并查看对新对象 foo 的引用(它 可以做到 ,尽管不能保证这样做是因为还没有 happens-before 关系)。
  4. 作者objects = objects;

不幸的是,这没有定义正式的 happens-before 关系,因为易失性变量读取 (3) 在易失性变量写入 (4) 之前。尽管 reader 可以偶然看到 objects[x] 是对象 foo ,但这并不意味着 foo 字段已安全发布,因此 reader 理论上可以看到新对象,但值错误!要解决这个问题,您使用此技术在线程之间共享的对象需要所有字段 finalvolatile 或以其他方式同步。例如,如果对象都是 Strings,你会没事的,否则,很容易出错。 (感谢@Holger 指出这一点。)


这里有一些不那么不稳定的选择:

  • AtomicReferenceArray 这样的并发数组 class 的存在是为了提供这样的数组,其中 每个元素 的行为就好像 volatile.这更容易正确使用,因为它确保如果 reader 看到更新的数组元素值,它也正确地看到该元素引用的对象。

  • 您可以将对数组的所有访问包装在 synchronized 块中,在某些共享对象上同步:

    // writer
    synchronized (aSharedObject) {
        objects[x] = foo;
    }
    
    // reader
    synchronized (aSharedObject) {
        bar = objects[x];
    }
    

    volatile 一样,使用 synchronized 创建 happens-before 关系。 (线程在释放对象的同步锁之前所做的一切 happens-before 任何其他线程获取同一对象的同步锁。)如果你这样做,你的数组不需要是 volatile.

  • 考虑一下数组是否真的是您在这里需要的。您还没有说明这些编写器和 reader 线程的用途,但是如果您想要某种生产者-消费者队列,那么您真正需要的 class 是 BlockingQueue or an Executor。您应该查看 Java 并发性 classes 看看它们中的一个是否已经满足您的需求,因为如果有,它肯定比 volatile 更容易正确使用。