Java - 不可变数组线程安全

发布于 2024-11-19 06:11:46 字数 633 浏览 2 评论 0原文

我有一个关于 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 来确保正确的行为,但如果可能的话我希望避免它。

I have a question regarding the Java Memory Model. Here is a simple class presenting the problem:

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

}

As far as I know the JMM guarantees that the value of final fields will be visible to other threads after construction. But I want to ensure that other threads will see the most recent version of data stored in the array after construction.

Of course the code above is just a simple example presenting the problem, actually I want to implement a simple cache for direct byte buffers and I wouldn't like to rely on some Collection classes. Currently I am using a ReentrantReadWriteLock to ensure the correct behaviour but I would like avoid it if it is possible.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

断爱 2024-11-26 06:11:46

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

Amir 提到 volatile 这通常很有用 - 但构造函数也有类似的语义 < code>final 确保可见性的变量。请参阅 JLS 条款 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.
    }
}

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

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

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

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

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

In this example, everything will be fine (hmm, let's suspend judgement a bit). Immutability is ambrosia when it comes to thread-safety - if a value cannot change, the majority of concurrency problems are immediately no longer a concern.

Amir mentioned volatile which is generally useful - but the constructor also has similar semantics for final variables that ensure visibility. See JLS clause 17.5 for details - essentially the constructor forms a happens-before relationship between the write to the final variables and any subsequent reads.

EDIT: So you set the values reference to the array in the constructor, it's visible across all threads at that point, and then it doesn't change. So we know all other threads will see the same array. But what about the array's contents?

As it stands, array elements don't have any special semantics with regard to volatility, they're as if you just declared a class yourself something like:

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

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

So - another thread will only see these variables if we can do something to establish the happens-before relationship. And if my understanding is correct this requires but a small change to your original code.

We already know that the setting of the array reference happens-before the end of the constructor. An additional point which is always true, is that actions in one thread happen-before later actions in that same thread. So we can combine these by setting the array fields first, and then assigning the final field, so as to get this transitive guarantee of visibility. This will of course require a temporary variable:

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

I think this is guaranteed to be safe, now that we've switched the seemingly irrelevant order of assignment and population.

But again, there might be something else I've missed which means the concurrency guarantees aren't as robust as hoped. This question is to my mind an excellent example of why writing bulletproof multithreaded code is tricky, even when you think you're doing something very simple, and how it takes a lot of thought and caution (and then bugfixes) to get right.

恋你朝朝暮暮 2024-11-26 06:11:46

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

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

your example is not quite right. in order to get the final field assurance, you need:

public ImmutableIntArray() {
    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
}
笑梦风尘 2024-11-26 06:11:46

我确实认为数组的语义与对象的最终引用相同。规范规定

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

它还说

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

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

I do think that you're afforded the same semantics with an array as with a final reference to an object. The spec states

A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

It also says

It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

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

青朷 2024-11-26 06:11:46

我认为你的数组更改将通过 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 Cookbook

您不能将构造函数内的final存储移至构造函数外部的存储下方,这可能会使对象对其他线程可见。 (如下所示,这可能还需要发出障碍)。同样,您不能使用第三个分配对前两个分配中的任何一个进行重新排序:
v.afield = 1; x.finalField = v; ...;共享引用 = x;

我认为这是由
(JSR-133 食谱)

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

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

您可以通过以下方式搜索:(JSR133规格)

I think your array changes will be visible with your ImmutableIntArray.
From my reading on JLS the [freeze] action should take place when constructor exits.
The use of a temp array I think is useless:

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

To obtain final field guaranties we will need a [freeze] somewhere before constructor exit:

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

Anyway, [freeze] leaves the gates open to reorder instructions above it, so we will have the same thing:

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

[freeze] is implemented to contain at minimum a [StoreStore]. This [StoreStore] barrier must be issued before the moment when instance constructed is published.

From JSR-133 Cookbook:

You cannot move stores of finals within constructors down below a store outside of the constructor that might make the object visible to other threads. (As seen below, this may also require issuing a barrier). Similarly, you cannot reorder either of the first two with the third assignment in:
v.afield = 1; x.finalField = v; ... ; sharedRef = x;

And I think this is done by
(JSR-133 Cookbook):

Issue a StoreStore barrier after all stores but before return from any constructor for any class with a final field.

So we can't store in sharedRef before all other contructor stores are done.

You can search by: "Transitive guarantees from final fields" in (JSR133 spec).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文