Java 如何";“安全发布”;延迟生成的有效不可变数组

Java 如何";“安全发布”;延迟生成的有效不可变数组,java,multithreading,final,Java,Multithreading,Final,Java当前的内存模型保证,如果对对象“George”的唯一引用存储在另一个对象“Joe”的final字段中,并且George和Joe都从未被任何其他线程看到,在存储之前对George执行的所有操作都将被所有线程视为在存储之前执行。如果在final字段中存储对对象的引用是有意义的,那么在这种情况下,这种方法非常有效,此后对象将永远不会发生变化 在假定延迟创建可变类型的对象的情况下(在拥有对象的构造函数完成执行后的某个时间),是否有任何有效的方法来实现这种语义?考虑一个相当简单的类 AlrayOu

Java当前的内存模型保证,如果对对象“George”的唯一引用存储在另一个对象“Joe”的
final
字段中,并且George和Joe都从未被任何其他线程看到,在存储之前对George执行的所有操作都将被所有线程视为在存储之前执行。如果在
final
字段中存储对对象的引用是有意义的,那么在这种情况下,这种方法非常有效,此后对象将永远不会发生变化

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

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];
    }
}
类数组{
最终int[]主数组;
数组内容(int[]initialContents){
mainArray=(int[])initialContents.clone();
}
公共整数getElementAt(整数索引){
返回主数组[索引];
}
int[]makeNewSumsArray(){
int[]temp=newint[mainArray.length+1];
整数和=0;
对于(int i=0;i您的第三个版本不起作用。为存储在
final
实例字段中的正确构造的对象所做的保证仅适用于该
final
字段的读取。由于其他线程不读取该
final
变量,因此没有做出任何保证

最值得注意的是,数组的初始化必须在数组引用存储到
最终持有者之前完成。它
变量没有说明
sumofevelopments3
变量的写入时间(如其他线程所示)。在实践中,JVM可能会优化整个
Holder
实例创建,因为它没有副作用,因此生成的代码的行为类似于
int[]
数组的普通不安全发布

要使用
final
字段发布保证,您必须发布包含
final
字段的
Holder
实例,无法绕过它


但是如果这个额外的实例让你恼火,你应该考虑使用一个简单的<代码> Value变量。毕竟,你只假设了那个<代码> Value变量的代价,换言之,考虑过早优化。

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



所以结论总是:测量,不要猜测。只有当你发现真正的瓶颈时才开始优化。

我认为你的第二个和第三个例子确实有效(就像你说的那样,引用本身可能不会被另一个线程注意到,这可能会重新分配数组。这是大量额外的工作!)

但这些例子基于一个错误的前提:一个
volatile
字段要求读者“注意”这一点是不正确的变化。事实上,
volatile
final
字段执行完全相同的操作。
volatile
final
的读取操作在大多数CPU架构上没有开销。我相信写
volatile
时有少量额外开销

所以我只想在这里使用volatile,而不用担心你所谓的“优化”。速度上的差异,如果有的话,将是非常微小的,我说的就像是用总线锁写的额外4个字节。你的“优化”代码读起来非常糟糕

次要的一点是,final字段并不要求您拥有对对象的唯一引用以使其不可变和线程安全。规范只要求您防止对对象进行更改。当然,拥有对对象的唯一引用是防止更改的一种方法。但是,已经不可变的对象(例如
java.lang.String
)可以毫无问题地共享

总之:。不要再胡说八道了,只需编写一个简单的数组更新,并将其赋值给
volatile

volatile int[] sumOfPrevElements;
public int getSumOfElementsBefore(int index) {
    if( sumOfPrevElements != null ) return sumOfPrevElements[index];
    sumOfPrevElements = makeNewSumsArray();
    return sumOfPrevElements[index];
}
如果线程#1存储一个对易失性字段的引用,然后在某个任意线程X上运行的一段代码将该引用复制到一个“普通”字段字段,然后由线程#2读取,发生之前关系是否仍然适用?如果X为2,它显然适用;如果X为3,我看不出线程3如何复制线程1尚未编写的引用。如果X恰好为1,则保证可能较弱,但当引用是由外部线程复制的,如果它是由创建它的线程复制的,则它将不起作用。“度量值不猜测”的问题它假定程序员的机器与运行程序的所有机器一样。x86内存模型在硬件中实现起来相当昂贵,Java的设计允许程序在硬件更简单的机器上运行。在今天的PC上,易失性读取是便宜的