10

我有一个关于 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 保证 final 字段的值在构造后对其他线程可见。但我想确保其他线程在构建后会看到存储在数组中的最新版本的数据。

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

4

4 回答 4

11

在这个例子中,一切都会好起来的(嗯,让我们暂停一下判断)。当涉及到线程安全时,不变性是不言而喻的——如果一个值不能改变,大多数并发问题立即不再是一个问题。

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

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

就目前而言,数组元素在波动性方面没有任何特殊语义,它们就像您自己声明了一个类一样:

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

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

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

我们已经知道数组引用的设置发生在构造函数结束之前。另一个始终正确的观点是,一个线程中的动作发生在同一线程中的后续动作之前。所以我们可以通过先设置数组字段,赋值final字段来组合这些,从而得到这种可见性的传递保证。这当然需要一个临时变量:

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.
}

我认为这可以保证是安全的,因为我们已经改变了看似无关的分配和填充顺序。

但同样,我可能错过了其他一些东西,这意味着并发保证并不像希望的那样强大。在我看来,这个问题是一个很好的例子,说明为什么编写防弹多线程代码很棘手,即使您认为自己正在做一些非常简单的事情,以及如何需要大量的思考和谨慎(然后是错误修复)才能正确。

于 2011-07-08T14:46:28.657 回答
3

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

public ImmutableIntArray() {
    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
}
于 2011-07-08T16:06:41.880 回答
2

我确实认为您可以为数组提供与对对象的最终引用相同的语义。规范指出

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

它还说

它还将看到至少与最终字段一样最新的最终字段引用的任何对象或数组的版本。

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

于 2011-07-08T16:17:10.787 回答
0

我认为您的数组更改将通过您的 ImmutableIntArray 可见。根据我对 JLS 的阅读,[freeze] 操作应该在构造函数退出时发生。我认为使用临时数组是没用的:

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

为了获得最终的字段保证,我们需要在构造函数退出之前的某个地方进行 [freeze]:

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

无论如何,[freeze] 让大门打开以重新排序上面的指令,所以我们会有同样的事情:

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

[freeze] 被实施为至少包含一个 [StoreStore]。这个 [StoreStore] 屏障必须在实例构造发布之前发出。

来自JSR-133 食谱

您不能将构造函数内的 final 存储向下移动到构造函数外部的存储之下,这可能使对象对其他线程可见。(如下所示,这可能还需要发布障碍)。类似地,你不能用第三个赋值对前两个中的任何一个重新排序:v.afield = 1; x.finalField = v; ... ; sharedRef = x;

我认为这是由 (JSR-133 Cookbook)完成的:

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

所以我们不能在所有其他构造函数存储完成之前存储在 sharedRef 中。

您可以通过(JSR133 规范)中的“最终字段的传递保证”进行搜索。

于 2014-12-12T14:24:12.867 回答