首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >连续并行流之间的Java缓存一致性?

连续并行流之间的Java缓存一致性?
EN

Stack Overflow用户
提问于 2018-08-25 01:10:32
回答 1查看 578关注 0票数 12

考虑下面这段代码(这并不完全是乍一看的样子)。

代码语言:javascript
复制
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“变量标记为易失性?

让我解释一下我目前是如何理解这一点的。

  • 在第一个并行流中,NumberContainer-123 (比方)被ForkJoinWorker-1 (比方说)递增。因此,ForkJoinWorker-1将具有最新的缓存NumberContainer-123.value,即1。(然而,其他fork-join工作线程将具有过期的NumberContainer-123.value缓存-它们将存储值0。在某个时刻,这些其他工作线程的缓存将被更新,但这不会立即发生。)
  • 第一个并行流结束,但公共的fork-join池工作线程不会被终止。然后,第二个并行流启动,使用完全相同的公共fork-join池工作线程。现在,假设在第二个并行流中,递增NumberContainer-123的任务被分配给
  • -2(比如说)。ForkJoinWorker-2将拥有自己的缓存值NumberContainer-123.value。如果在NumberContainer-123的第一次和第二次递增之间经过了很长一段时间,那么假设ForkJoinWorker-2的NumberContainer-123的缓存将是最新的,即值1将被存储,并且一切正常。但是,如果NumberContainer-123非常短,那么第一次递增和第二次递增之间的时间会怎么样呢?那么可能ForkJoinWorker-2的NumberContainer-123缓存可能已过期,存储的值为0,从而导致代码失败!

我上面的描述正确吗?如果是这样的话,谁能告诉我两个递增操作之间需要什么样的时间延迟才能保证线程之间的缓存一致性?或者,如果我的理解是错误的,那么谁能告诉我是什么机制导致线程本地缓存在第一个并行流和第二个并行流之间被“刷新”?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2018-08-25 01:33:03

它应该不需要任何延迟。当你在ParallelStreamforEach之外的时候,所有的任务都已经完成了。这在增量和forEach的结尾之间建立了一个发生在之前的关系。所有的forEach调用都是按照从同一个线程调用的顺序进行的,同样,检查也发生在所有的forEach调用之后。

代码语言:javascript
复制
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标记为易失性来完成的,但这是一个您不应该关心或依赖的实现细节。

票数 5
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/52009032

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档