【问题标题】:Volatile arrays and memory barriers and visibility in JavaJava中的易失性数组和内存屏障以及可见性
【发布时间】:2017-01-17 10:37:06
【问题描述】:

我很难理解 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];
}

我的问题是,这是否符合我的想法,即“发布”到memoryBarrier 并随后读取变量强制缓存一致性操作,并确保读取器线程确实会得到 both 最新的数组引用和指定索引处的正确基础值?

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

编辑:恰好有一个写入线程和许多读取线程。

【问题讨论】:

    标签: java multithreading volatile memory-barriers


    【解决方案1】:

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

    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 取消引用,都将按照预期进行观察,并更新索引。

    【讨论】:

    • 我的场景非常具体,我无法详细说明,但实际上我不需要经典意义上的线程安全,因为阅读器线程甚至永远无法请求索引以前未发布到阵列。发生的情况是:读者总是知道可用的最大索引,但它不知道该索引的基础值。我的代码是否能保证读者总是获得both 最新的数组引用 该索引处的基础值(而不是陈旧的值)?这是一个可见性问题,而不是线程安全本身。
    • 不确定可见性和线程安全之间的区别是什么;通常非保证可见性算作线程不安全,反之亦然。正如我所描述的,您的程序包含一个比赛,这意味着您不能保证同时观察到最新的数组引用和索引处的值。我正在添加更多代码,以向您展示安全发布的正确习语。
    • 糟糕...另一个答案已经做到了。他甚至决定使用相同的变量名:)
    • 感谢更新示例。我注意到您删除了内存屏障字段并声明数组本身是易失的。如果我们对多个数组执行此操作,而不是将每个数组都声明为易失性(易失性写入很昂贵),我们是否可以简单地处理它们,然后简单地写入单个易失性字段以确保可见性传播?
    • 不,这会像您的原始代码一样被破坏。您可以通过使用AtomicReferenceFieldUpdater 并改为调用lazySet 来消除易失性写入的成本。这保留了写入顺序保证。
    【解决方案2】:

    一般来说它不起作用的原因有很多:

    • Java 没有说明内存屏障或顺序 不相关变量。全局内存屏障是一个副作用 x86
    • 即使有全局内存屏障:数组引用和索引数组值的写入顺序未定义。保证两者都发生在内存屏障之前,但是以什么顺序?非同步读取可能会看到引用,但看不到数组值。在多次读/写的情况下,您的读屏障在这里没有帮助。
    • 注意引用数组:引用值的可见性需要特别注意

    更好的方法是将数组本身声明为 volatile将其值视为不可变:

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

    【讨论】:

    • Java 确实说明了无关变量的内存屏障,因为happens-before 关闭了程序顺序。
    【解决方案3】:

    除非您声明变量volatile,否则无法保证线程将获得正确的值。易失性保证变量的变化是可见的,而不是使用 CPU 缓存,它将从主内存写入/读取。 您还需要synchronization,以便在写入完成之前读取线程不会读取。有什么理由使用数组而不是 ArrayList 对象,因为您已经在使用 Arrays.copyOf 并调整大小?

    【讨论】:

    • 将数组声明为 volatile 将数组引用标记为 volatile,但不是数组值。
    猜你喜欢
    • 1970-01-01
    • 2016-05-31
    • 2019-11-09
    • 2020-02-08
    • 1970-01-01
    • 2010-12-19
    • 2018-02-28
    • 2013-05-23
    • 2023-03-21
    相关资源
    最近更新 更多