考虑下面的代码(乍看之下并不太像)。

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 ?

让我解释一下我目前的理解。
  • 在第一个并行流中,NumberContainer-123(例如)由ForkJoinWorker-1(例如)递增。因此,ForkJoinWorker-1将具有NumberContainer-123.value的最新缓存,即1。(但是,其他fork-join worker 将具有NumberContainer-123.value的最新缓存-他们将存储值0。在某些时候,这些其他工作程序的缓存将被更新,但这不会立即发生。)
  • 第一个并行流完成,但是公共(public)的fork-join池工作线程没有被杀死。然后,使用完全相同的公共(public)fork-join池工作线程启动第二个并行流。
  • 现在,假设在第二个并行流中,将NumberContainer-123递增的任务分配给ForkJoinWorker-2(例如)。 ForkJoinWorker-2将具有自己的NumberContainer-123.value缓存值。如果在NumberContainer-123的第一个和第二个增量之间经过了一段较长的时间,则大概ForkJoinWorker-2的NumberContainer-123.value的缓存将是最新的,即将存储值1,并且所有内容好的。但是,如果NumberContainer-123非常短,那么在第一和第二增量之间经过的时间该怎么办?然后,也许ForkJoinWorker-2的NumberContainer-123.value的缓存可能已过期,存储的值为0,从而导致代码失败!

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

    最佳答案

    它不需要任何延迟。到ParallelStreamforEach不足时,所有任务都已完成。这将在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来完成的,但这是您不应该关心或依赖的实现细节。

    07-24 13:51