Java 中的易失性数组和内存屏障以及可见性

Volatile arrays and memory barriers and visibility in Java

我很难理解 Java 中的内存屏障和缓存一致性,以及这些概念与数组的关系。

我有以下场景,其中一个线程修改数组(对它的引用和它的一个内部值),另一个线程从中读取。

int[] integers;
volatile boolean memoryBarrier;

public void resizeAndAddLast(int value) {
    integers = Arrays.copyOf(integers, integers.size + 1);
    integers[integers.size - 1] = value;
    memoryBarrier = true;
}

public int read(int index) {
    boolean memoryBarrier = this.memoryBarrier;
    return integers[index];
}

我的问题是,这是否按照我认为的那样进行,即从 "publishing" 到 memoryBarrier 然后读取变量强制执行缓存一致性操作并确保 reader 线程确实会 最新的数组引用和指定索引处的正确基础值?

我的理解是数组引用不必声明 volatile,使用 any volatile 字段应该足以强制执行缓存一致性操作。这个推理正确吗?

编辑:只有一个作者线程和许多 reader 个线程。

除非您声明变量 volatile,否则无法保证线程会获得正确的值。 Volatile 保证变量的变化是可见的,这意味着它不会使用 CPU 缓存,而是 write/read 来自主内存。 您还需要 synchronization 以便读取线程在写入完成之前不会读取。因为您已经在使用 Arrays.copyOf 并调整大小,所以使用数组而不是 ArrayList 对象的任何原因?

不,您的代码是线程不安全的。使其安全的变体如下:

void raiseFlag() {
   if (memoryBarrier == true)
     throw new IllegalStateException("Flag already raised");
   memoryBarrier = true;
}

public int read(int index) {
  if (memoryBarrier == false)
     throw IllegalStateException("Flag not raised yet");
  return integers[index];
}

您只能升旗一次,并且不能发布超过一个 integers 阵列。不过,这对您的用例来说毫无用处。

现在,至于 为什么 ... 您不能保证在 read() 的第一行和第二行之间没有对 integers 由第二行观察到。缺少内存屏障不会阻止 另一个线程观察一个动作。它使结果未指定。

有一个简单的习惯用法可以使您的代码线程安全(专用于假设单个线程调用 resizeAndAddLast,否则需要更多代码和 AtomicReference):

volatile int[] integers;

public void resizeAndAddLast(int value) {
    int[] copy = Arrays.copyOf(integers, integers.length + 1);
    copy[copy.length - 1] = value;
    integers = copy;
}

public int read(int index) {
    return integers[index];
}

在此代码中,一旦数组发布,您就永远不会触及它,因此无论您从 read 取消引用什么,都将按预期进行观察,并更新索引。

它通常无法正常工作的原因有多种:

  • Java 没有说任何关于内存障碍或排序的事情 不相关 个变量。全局内存屏障是 x86
  • 即使有全局内存屏障:数组引用和索引数组值的写入顺序未定义。可以保证两者都发生在内存屏障之前,但是顺序是什么?非同步读取可能会看到引用,但看不到数组值。如果有多个 read/writes.
  • ,您的阅读障碍在这里无济于事
  • 当心引用数组:引用值的可见性需要特别注意

稍微好一点的方法是将数组本身声明为易变的并且将其值视为不可变的:

volatile int[] integers; // volatile (or maybe better AtomicReference)

public void resizeAndAddLast(int value) {
    // enforce exactly one volatile read!
    int[] copy = integers;
    copy = Arrays.copyOf(copy, copy.size + 1);
    copy[copy.size - 1] = value; 
    // may lose concurrent updates. Add synchronization or a compareExchange-loop!
    integers = copy;
}

public int read(int index) {
    return integers[index];
}