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

St.*_*rio 18 java arrays multithreading volatile

我有一个包含对数组的volatile引用的类:

private volatile Object[] objects = new Object[100];
Run Code Online (Sandbox Code Playgroud)

现在,我可以保证,只有一个线程(调用它writer)可以写入数组.例如,

objects[10] = new Object();
Run Code Online (Sandbox Code Playgroud)

所有其他线程只读取线程写入的值writer.

问题:我是否需要同步这样的读写以确保内存一致性?

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

Ale*_*you 14

你可以使用AtomicReferenceArray:

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

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

// reader
Object obj = objects.get(10);
Run Code Online (Sandbox Code Playgroud)

这将确保原子更新和发生 - 在读/写操作的一致性之前,就像每个数组项一样volatile.

  • 或同步,信号量或其他任何满足JVM内存模型的东西. (3认同)
  • @EJP肯定,但包含所有可能选项的答案太大了.我试图适应OP的模型(现在他们使用数组并需要对其项目进行原子更新,如果我正确理解了这个问题). (3认同)

Coo*_*tri 13

private volatile Object[] objects = new Object[100];
Run Code Online (Sandbox Code Playgroud)

你只能objects参考volatile这种方式.不是关联的数组实例内容.

问题:我是否需要同步这样的读写以确保内存一致性?

是.

如果JVM在写入数组时提供某种内存一致性保证,那么从性能角度来看它是没有用的

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

Java平台也有Vector(过时的设计有缺陷)和synchronized List(很多场景都很慢),但我不建议使用它们.

PS: 来自@SashaSalauyou的另一个好主意

  • `Vector`是一个可怕的例子,`Collections.synchronizedList`也是如此.一个是过时的,一个是非常天真的实现.看一下[`java.util.concurrent`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html)包.真的,这个答案都是错的. (3认同)

Boa*_*ann 8

根据JLS§17.4.5 - 在订单之前发生:

可以通过先发生关系来排序两个动作.如果一个动作发生在另一个动作之前,则第一个动作第二个动作之前可见并在第二个之前被命

[...]

对该字段的每次后续读取之前发生volatile字段的写入.

之前发生关系是相当强的.这意味着如果线程A写入volatile变量,并且任何线程B稍后读取变量,则线程B保证看到volatile变量本身的变化,以及在设置变量之前所做的每个其他变更线程A volatile,包括是否是其他任何对象volatile.

但是,这还不够!

元素分配objects[10] = new Object();不是写的变量objects.它只是读取变量以确定它指向的数组,然后写入包含在位于内存中其他位置的数组对象中的不同变量.没有发生 - 在仅通过读取volatile变量建立关系之前,因此代码不安全.

正如@DimitarDimitrov指出的那样,你可以通过对objects变量进行虚拟写操作来解决这个问题.每一对操作 - objects = objects;作者线程的重新分配以及foo = objects[x];读取器线程的查找 - 定义更新的先发生关系,因此将"发布"作者线程对读取器线程所做的所有最新更改.这可行,但它需要纪律,而且不优雅.

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

  1. Writer创建了一些对象foo.
  2. 作家集 objects[x] = foo;
  3. Reader检查objects[x]并查看对新对象的引用foo(它可以执行,但不保证这样做,因为之前没有发生关系).
  4. 作家呢 objects = objects;

不幸的是,这并没有定义正式发生在之前的关系,因为volatile变量read(3)来自volatile变量write(4).虽然读者可以看到这objects[x]foo偶然的对象,但这并不意味着安全发布字段foo,所以理论上读者可能会看到新的对象,但错误的值!为了解决这个问题,你使用这种技术的线程之间共享的对象需要有各个领域finalvolatile或以其他方式同步.String例如,如果对象都是s,那么你会没事的,但除此之外,用它来犯错也太容易了.(感谢@Holger指出这一点.)


以下是一些不太明显的替代品:

  • 并发数组类AtomicReferenceArray存在以提供其中每个元素都表现得像的数组volatile.这更容易正确使用,因为它确保如果读者看到更新的数组元素值,它还可以正确地看到该元素引用的对象.

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

    // writer
    synchronized (aSharedObject) {
        objects[x] = foo;
    }
    
    Run Code Online (Sandbox Code Playgroud)
    // reader
    synchronized (aSharedObject) {
        bar = objects[x];
    }
    
    Run Code Online (Sandbox Code Playgroud)

    就像volatile,使用synchronized创造一个发生在之前的关系.(在释放对象的同步锁之前,线程所做的一切都发生在任何其他线程获取同一对象的同步锁之前.)如果这样做,则不需要数组volatile.

  • 考虑一下阵列是否真的是你需要的.你还没有说出这些作者和读者线程的用途,但是如果你想要某种生产者 - 消费者队列,那么你真正需要的类是a BlockingQueue或者Executor.你应该查看Java并发类,看​​看其中一个是否已经完成了你需要的东西,因为如果有的话,它肯定比正确使用更容易volatile.

  • @Sasha Salauyou:这是对"易变"变量的大量误解之一.似乎很多开发人员都试图变得聪明,而且大多数时候他们甚至都没有检查这些"聪明的技巧"是否会获得任何性能上的好处,更不用说正确性了......根据经验,如果写一下没有可观察到的效果(比如再次写一个旧值时,它无法与读者建立一个先发生过的关系.如果它一般不能建立这种关系,JVM可以自由地完全忽略它. (2认同)

Dim*_*rov 7

是的,您需要同步访问易失性数组的元素.

其他人已经解决了你可能会如何使用CopyOnWriteArrayListAtomicReferenceArray代替,所以我将转向一个稍微不同的方向.我还建议由JMM的一大贡献者Jeremy Manson 阅读Java中Volatile Arrays.

现在,我可以保证只有一个线程(称为编写器)可以写入数组,如下所示:

是否可以提供单个作者保证与volatile关键字无关.我认为你没有考虑到这一点,但我只是澄清,以便其他读者不会得到错误的印象(我认为有一个数据竞争双关语,可以用这句话).

所有其他线程只读取编写器线程写入的值.

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

这意味着要么你想要做的事情

objects[i] = newObj;
objects = objects;
Run Code Online (Sandbox Code Playgroud)

这在许多方面都是丑陋可怕的.或者你想在每次作家进行更新时发布一个全新的数组,例如

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;
Run Code Online (Sandbox Code Playgroud)

这不是一个非常常见的用例.

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

因为如果JVM在写入数组时提供某种内存一致性保证,从性能角度来看是没有用的.但我不确定.

就像你提到的那样,确保volatile语义所需的内存防护成本非常高,如果你在混合中添加错误共享,它就会变得更糟.

没有找到任何有用的文档.

您可以放心地假设volatile数组引用的volatile语义与非数组引用的语义完全相同,这一点都不奇怪,考虑到数组(甚至原始数组)仍然是对象.

  • 像`objects = objects;`这样的虚拟写入是*不够的.读取线程可以在`objects [i] = newObj;`和`objects = objects;`语句之间读取数组引用,此时,没有发生在之前的关系.认为执行`volatile`读取的线程将等待另一个线程执行其`volatile`写入似乎是一个常见的错误.通过`volatile`变量发布仅在应用程序可以处理读取器读取旧值的可能性时起作用,例如"发布全新阵列"解决方案. (3认同)