Java - 不可变数组线程安全

时间:2011-07-08 14:40:40

标签: java concurrency thread-safety immutability java-memory-model

我对Java内存模型有疑问。这是一个介绍问题的简单类:

public class ImmutableIntArray {

    private final int[] array;

    public ImmutableIntArray() {
        array = new int[10];
        for (int i = 0; i < 10; i++) {
            array[i] = i;
        }
    }

    // Will always return the correct value?
    public int get(int index) {
        return array[index];
    }

}

据我所知,JMM保证在构造之后其他线程可以看到最终字段的值。但是我希望确保其他线程在构建之后能够看到存储在数组中的最新数据版本。

当然上面的代码只是一个呈现问题的简单示例,实际上我想为直接字节缓冲区实现一个简单的缓存,我不想依赖一些Collection类。目前我正在使用ReentrantReadWriteLock来确保正确的行为,但如果可能的话我想避免它。

4 个答案:

答案 0 :(得分:11)

在这个例子中,所有都没问题(嗯,让我们暂停一下判断)。在线程安全性方面,不变性是ambrosia - 如果值不能改变,大多数并发问题立即不再是一个问题。

Amir提到了volatile这通常很有用 - 但构造函数也有类似的final变量语义,以确保可见性。有关详细信息,请参阅JLS clause 17.5 - 基本上构造函数在写入最终变量和任何后续读取之间形成发生之前关系。

编辑:因此,您在构造函数中设置了对数组的 values 引用,它在该点的所有线程中都可见,然后它不会更改。所以我们知道所有其他线程都会看到相同的数组。但是数组的内容呢?

就目前而言,数组元素在波动性方面没有任何特殊的语义,它们就好像你只是自己宣布一个类:

public class ArrayTen {
    private int _0;
    private int _1;
    // ...
    private int _9;

    public int get(int index) {
       if (index == 0) return _0;
       // etc.
    }
}

所以 - 如果我们可以做一些事情来建立发生在之前的关系,另一个线程将只看到这些变量。如果我的理解是正确的,那么只需要对原始代码进行一些小改动。

我们已经知道数组引用的设置发生在构造函数结束之前。另一个始终正确的观点是,一个线程中的操作发生在之后的同一线程中的操作之前。因此,我们可以通过首先设置数组字段来组合这些,然后然后分配最终字段,以便获得可见性的传递保证。这当然需要一个临时变量:

public class ImmutableIntArray {

    private final int[] array;

    public ImmutableIntArray() {
        int[] tmp = new int[10];
        for (int i = 0; i < 10; i++) {
            tmp[i] = i;
        }
        array = tmp;
    }

    // get() etc.
}

我认为这是安全的,因为我们已经改变了看似无关的任务和人口顺序。

但同样,可能还有一些我错过的东西,这意味着并发保证不像希望的那样强大。这个问题在我看来是一个很好的例子,为什么编写防弹多线程代码是棘手的,即使你认为你做的事情非常简单,以及需要多少思考和谨慎(以及错误修正)才能做到正确。 / p>

答案 1 :(得分:3)

你的例子不太对劲。为了获得最终的现场保证,您需要:

public ImmutableIntArray() {
    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
}

答案 2 :(得分:2)

我确实认为你为数组提供了与对象最终引用相同的语义。规范说明

  

在该对象完全初始化之后只能看到对象引用的线程可以保证看到该对象的最终字段的正确初始化值。

它也说

  

它还会看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。

http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5

答案 3 :(得分:0)

我认为使用ImmutableIntArray可以看到您的数组更改。 从我对JLS的阅读中,当构造函数退出时,[冻结]动作应该发生。 我认为使用临时数组是没用的:

int tmparray = new int[10];
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
array = tmparray;

要获得最终的字段保证,我们需要在构造函数退出之前的某处[冻结]:

int tmparray = new int[10];
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
array = tmparray;
[freeze]

无论如何,[冻结]让大门敞开以重新排列它上面的指令,所以我们会有同样的事情:

int tmparray = new int[10];
array = tmparray; 
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
[freeze]

[冻结]实现至少包含[StoreStore]。必须在发布实例构造之前发布此[StoreStore]屏障。

来自JSR-133 Cookbook

  

您无法将构造函数中的终结存储库移动到构造函数外部的存储库之下,该存储库可能使该对象对其他线程可见。 (如下所示,这可能还需要发布屏障)。同样,您不能使用第三个赋值重新排序前两个中的任何一个:         v.afield = 1; x.finalField = v; ......; sharedRef = x;

我认为这是通过 (JSR-133 Cookbook)

  

在所有商店之后但在从具有最终字段的任何类的任何构造函数返回之前发出StoreStore屏障。

因此,在完成所有其他构造函数存储之前,我们无法存储在sharedRef中。

您可以在(JSR133 spec)中搜索:“最终字段的传递保证”。