考虑下面这段代码(这并不完全是乍一看的样子)。
static class NumberContainer {
int value = 0;
void increment() {
value++;
}
int getValue() {
return value;
}
}
public static void main(String[] args) {
List<NumberContainer> list = new ArrayList<>();
int numElements = 100000;
for (int i = 0; i < numElements; i++) {
list.add(new NumberContainer());
}
int numIterations = 10000;
for (int j = 0; j < numIterations; j++) {
list.parallelStream().forEach(NumberContainer::increment);
}
list.forEach(container -> {
if (container.getValue() != numIterations) {
System.out.println("Problem!!!");
}
});
}
我的问题是:为了绝对确定“问题!”是否需要将NumberContainer类中的"value“变量标记为易失性?
让我解释一下我目前是如何理解这一点的。
我上面的描述正确吗?如果是这样的话,谁能告诉我两个递增操作之间需要什么样的时间延迟才能保证线程之间的缓存一致性?或者,如果我的理解是错误的,那么谁能告诉我是什么机制导致线程本地缓存在第一个并行流和第二个并行流之间被“刷新”?
发布于 2018-08-25 01:33:03
它应该不需要任何延迟。当你在ParallelStream
的forEach
之外的时候,所有的任务都已经完成了。这在增量和forEach
的结尾之间建立了一个发生在之前的关系。所有的forEach
调用都是按照从同一个线程调用的顺序进行的,同样,检查也发生在所有的forEach
调用之后。
int numIterations = 10000;
for (int j = 0; j < numIterations; j++) {
list.parallelStream().forEach(NumberContainer::increment);
// here, everything is "flushed", i.e. the ForkJoinTask is finished
}
回到你关于线程的问题,这里的诀窍是,线程是无关的。内存模型依赖于发生-之前关系,fork-join任务确保forEach
调用与操作体之间、操作体与forEach
返回之间存在发生-之前关系(即使返回值为Void
)。
另请参阅Memory visibility in Fork-join
正如@erickson在评论中提到的,
如果你不能通过发生的事情来建立正确性--在建立关系之前,再多的时间也是“不够的”。这不是一个时钟计时问题;您需要正确地应用Java内存模型。
此外,从“刷新”内存的角度来思考它是错误的,因为有更多的事情可以影响你。例如,刷新是微不足道的:我没有检查过,但我敢打赌,在任务完成上只有一个内存屏障;但你可能会得到错误的数据,因为编译器决定优化非易失性读取(变量不是易失性的,并且在这个线程中不会改变,所以它不会改变,所以我们可以将它分配给一个寄存器,等等),以发生之前关系允许的任何方式重新排序代码,等等。
最重要的是,所有这些优化都可以而且将随着时间的推移而改变,所以即使您访问了生成的程序集(根据加载模式的不同而有所不同)并检查了所有的内存屏障,也不能保证您的代码能够正常工作,除非您能够证明您的读取发生了--在您写入之后,在这种情况下,Java memory Model站在您这边(假设JVM中没有bug )。
至于巨大的痛苦,ForkJoinTask
的目标就是让同步变得微不足道,所以请尽情享受。它(看起来)是通过将java.util.concurrent.ForkJoinTask#status
标记为易失性来完成的,但这是一个您不应该关心或依赖的实现细节。
https://stackoverflow.com/questions/52009032
复制相似问题