java - 如何 "safely publish"延迟生成的有效不可变数组

标签 java multithreading final

Java 目前的内存模型保证,如果对对象“George”的唯一引用存储在 final 中。某个其他对象“Joe”的字段,并且 George 和 Joe 都从未被任何其他线程看到,所有在存储之前对 George 执行的操作都将被所有线程视为在存储之前执行过。这在需要存储到 final 的情况下非常有效。 field 对一个对象的引用,此后将永远不会发生变异。

在应该延迟创建可变类型对象的情况下(在拥有对象的构造函数完成执行之后的某个时间),是否有任何有效的方法来实现这种语义?考虑相当简单的类 ArrayThing它封装了一个不可变数组,但它提供了一种方法(具有相同名义目的的三个版本)来返回指定元素之前的所有元素的总和。出于本示例的目的,假设将在不使用该方法的情况下构造许多实例,但是在使用该方法的实例上,它将被大量使用;因此,当 ArrayThing 的每个实例时预先计算总和是不值得的。已构造,但值得缓存它们。

class ArrayThing {
    final int[] mainArray;

    ArrayThing(int[] initialContents) {
        mainArray = (int[])initialContents.clone();
    }
    public int getElementAt(int index) {
        return mainArray[index];
    }

    int[] makeNewSumsArray() {
        int[] temp = new int[mainArray.length+1];
        int sum=0;
        for (int i=0; i<mainArray.length; i++) {
            temp[i] = sum;
            sum += mainArray[i];
        }
        temp[i] = sum;
        return temp;
    }

    // Unsafe version (a thread could be seen as setting sumOfPrevElements1
    // before it's seen as populating array).

    int[] sumOfPrevElements1;
    public int getSumOfElementsBefore_v1(int index) {
        int[] localElements = sumOfPrevElements1;
        if (localElements == null) {
            localElements = makeNewSumsArray();
            sumOfPrevElements1 = localElements;
        }
        return localElements[index];
    }
    static class Holder {
        public final int[] it;
        public Holder(int[] dat) { it = dat; }
    }

    // Safe version, but slower to read (adds another level of indirection
    // but no thread can possibly see a write to sumOfPreviousElements2
    // before the final field and the underlying array have been written.

    Holder sumOfPrevElements2;
    public int getSumOfElementsBefore_v2(int index) {
        Holder localElements = sumOfPrevElements2;
        if (localElements == null) {
            localElements = new Holder(makeNewSumsArray());
            sumOfPrevElements2 = localElements;
        }
        return localElements.it[index];
    }

    // Safe version, I think; but no penalty on reading speed.
    // Before storing the reference to the new array, however, it
    // creates a temporary object which is almost immediately
    // discarded; that seems rather hokey.

    int[] sumOfPrevElements3;
    public int getSumOfElementsBefore_v3(int index) {
        int[] localElements = sumOfPrevElements3;
        if (localElements == null) {
            localElements = (new Holder(makeNewSumsArray())).it;
            sumOfPrevElements3 = localElements;
        }
        return localElements[index];
    }
}

String#hashCode() 一样方法,两个或多个线程可能会看到尚未执行的计算,决定执行它并存储结果。由于所有线程最终都会产生相同的结果,因此这不会成为问题。与 getSumOfElementsBefore_v1() ,然而,有一个不同的问题:Java 可以重新排序程序执行,因此数组引用被写入 sumOfPrevElements1在写入数组的所有元素之前。另一个线程调用 getSumOfElementsBefore()此时可以看到数组不为空,然后继续读取尚未写入的数组元素。哎呀。

据我了解,getSumOfElementsBefore_v2()解决了这个问题,因为在最终字段 Holder#it 中存储了对数组的引用将建立关于数组元素写入的“发生后”关系。不幸的是,该版本的代码需要创建和维护一个额外的堆对象,并且要求每次访问元素和数组的尝试都经过额外的间接级别。

我想getSumOfElementsBefore_v3()会更便宜但仍然安全。 JVM 保证在引用之前对新对象执行的所有操作都存储到 final 中。字段将在任何线程可以看到该引用时对所有线程可见。因此,即使其他线程不使用 Holder#it直接地,他们正在使用从该字段复制的引用这一事实将表明,在商店实际发生之前完成的所有操作之后,他们才能看到该引用。

尽管后一种方法将开销(相对于不安全方法)限制在创建新数组的时间(而不是为每次读取增加开销),但纯粹为了写入和目的而创建新对象仍然显得相当丑陋读回最后一个字段。制作数组字段volatile将实现合法的语义,但每次读取该字段时都会增加内存系统开销(volatile 限定符将要求代码通知该字段是否已写入另一个线程,但这对这个应用程序来说太过分了;必要的是只是看到该字段已被写入的任何线程也会看到在存储引用之前对数组进行的所有写入识别)。有没有什么方法可以实现类似的语义,而不必创建和放弃多余的临时对象,或者每次读取字段时都增加额外的开销??

最佳答案

您的第三个版本不起作用。为存储在 final 中的正确构造的对象所做的保证实例字段适用于读取该 final仅限字段。由于其他线程不读取 final变量,不做任何保证。

最值得注意的是,数组的初始化必须在数组引用存储在 final Holder.it 之前完成。变量没有说明何时 sumOfPrevElements3变量将被写入(如其他线程所见)。实际上,JVM 可能会优化掉整个 Holder实例创建,因为它没有副作用,因此结果代码的行为就像 int[] 的普通不安全发布大批。

用于使用 final现场发表保证您必须发表Holder包含 final 的实例领域,没有办法绕过它。

但是如果那个额外的实例让你烦恼,你真的应该考虑使用一个简单的 volatile多变的。毕竟,您只是对 volatile 的成本做出了假设。变量,换句话说,考虑过早优化。

毕竟,检测另一个线程所做的更改不一定很昂贵,例如在 x86 上,它甚至不需要访问主内存,因为它具有缓存一致性。也有可能优化器检测到一旦变量变为非 null,您就再也不会写入该变量。 ,然后为普通字段启用几乎所有可能的优化,一旦非 null引用已阅读。

所以结论一如既往:衡量,不要猜测。并且只有在发现真正的瓶颈后才开始优化。

关于java - 如何 "safely publish"延迟生成的有效不可变数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27278797/

相关文章:

java - 无法引用不同方法中定义的内部类内的非最终变量 lblNewLabel

java - 如何使用列表的最终实例设计链表

java - 如果您的数据库层只有 JDBC,是否值得使用 Spring?

java - 可以返回什么类型而不是 null?

c# - Thread.Sleep() sleep 时间更长

c++ - 为什么在将临时变量传递给线程函数时移动构造函数会被调用两次?

javascript - 读取本地文件 react native

java - 通过将 Spring MVC 与 AJAX/JSON 和 MappingJackson 结合使用来引发 LazyInitializationException

c - OpenCL:如何在不使用多线程的情况下在不同设备上分配计算

scala - Scala 构造函数上的“发生在”之前 : final fields