考虑下面的代码(乍看之下并不太像)。
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类中的“值”变量标记为 volatile ?
让我解释一下我目前的理解。
我上面的描述正确吗?如果是这样,谁能告诉我两次增量操作之间需要什么样的时间延迟才能保证线程之间的缓存一致性?或者,如果我的理解是错误的,那么有人可以告诉我是什么机制导致线程并行缓存在第一并行流和第二并行流之间“刷新”吗?
最佳答案
它不需要任何延迟。到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在评论中提到的那样,
而且,从“刷新”内存的角度考虑问题是错误的,因为还有更多的事情会影响您。例如,冲洗是微不足道的:我没有检查过,但可以打赌,完成任务只是内存上的障碍。但是您会得到错误的数据,因为编译器决定优化非 volatile 读取(变量不是volatile,并且在该线程中未更改,因此它不会更改,因此我们可以将其分配给寄存器,等等) ),以“事前发生”关系允许的任何方式对代码进行重新排序等。
最重要的是,所有这些优化都会随着时间的推移而变化,因此,即使您转到生成的程序集(根据加载模式的不同而有所不同)并检查了所有内存障碍,也不能保证您的代码会正常工作,除非您可以证明您的读操作在写操作之后发生,在这种情况下,Java内存模型就在您身边(假设JVM中没有错误)。
对于巨大的痛苦,
ForkJoinTask
的目标是使同步变得微不足道,因此请尽情享受。 (看起来)是通过将java.util.concurrent.ForkJoinTask#status
标记为volatile来完成的,但这是您不应该关心或依赖的实现细节。