Fork-join中的内存可见性

7
Brian Goetz在http://www.ibm.com/developerworks/java/library/j-jtp03048.html上发表了一篇关于fork-join的好文章。在文章中,他使用fork-join机制列出了一个归并排序算法,其中他同时对数组的两侧进行排序,然后合并结果。
该算法同时对同一数组的两个不同部分进行排序。为什么不需要AtomicIntegerArray或其他机制来维护可见性?有什么保证一个线程会看到另一个线程所做的写操作,还是这是一个微妙的错误?作为后续问题,Scala的ForkJoinScheduler是否也提供此保证?
谢谢!

它们正在处理数组的不同部分。在合并之前没有争用。 - Anon.
3
我同意它们正在处理不同的部分。但是,Java内存模型的语义更或多或少地表明,并非所有线程都可以保证看到所有写操作(除非变量是volatile)。根据这篇博客:http://jeremymanson.blogspot.com/2009/06/volatile-arrays-in-java.html,即使使用一个volatile int[]也不能保证其他线程可以看到对数组的写入。 - Joshua Hartman
2个回答

9

加入(ForkJoin的)本身需要同步点,这是最重要的信息。同步点将确保在该点之后发生的所有写操作都可见。

如果您查看代码,可以看到同步点发生的位置。这只是一个方法调用invokeAll。

public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
    t2.fork();
    t1.invoke();
    t2.join();
}

在这里,t2分叉成另一个进程,t1执行其任务并等待t2.join()的调用线程。传递t2时,所有对t1和t2的写操作将变得可见。

编辑:此编辑仅是为了更好地解释我所说的同步点。

假设您有两个变量

int x;
volatile int y;

任何时候,当你写入 y 的时候,所有在你读取 y 之前发生的写操作都会被保存。例如:

public void doWork(){
   x = 10;
   y = 5;
}

如果另一个线程读取了 y = 5,那么该线程保证会读取 x = 10。这是因为对 y 的写入创建了一个同步点,在该点之前的所有写入将在写入后可见。
使用 Fork Join 池时,ForkJoinTask 的 join 将创建一个同步点。现在,如果 t2.fork() 和 t1.invoke(),则 t2 的 joining 将确保之前发生的所有写入都将被看到。由于所有先前的写入都在同一结构内,因此对于可见性来说是安全的。
如果这不够清晰,我很乐意进一步解释。

invokeAll 是由 coInvoke 调用的吗? - Daniel C. Sobral
此外,为了补充答案,还可以参考http://java.sun.com/docs/books/jls/third_edition/html/memory.html#64058。 - Daniel C. Sobral
@Danciel C. Sobral 很有趣的问题,当我写这个例子时,我实际上正在寻找 coInvoke 方法。看起来 coInvoke 方法本身已经从源代码中清除了。至少我再也找不到它了。 - John Vint
根据Daniel发布的文档:“线程中的所有操作都发生在任何其他线程成功从该线程的join()返回之前。”我开始自己阅读源代码 - 这非常棘手。虽然看起来join保证了可见性。我很想了解更多细节,但我会将此答案标记为正确。 - Joshua Hartman
@Joshua Hartman,随意查看我的编辑以获取更详细的说明。 - John Vint

1

只是猜测:合并包括在线程上加入,而加入保证了可见性。

第二部分是确定的;我不知道合并是如何实现的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接