这个问题是在另一个问题的回答中作为“副作用”出现的,更多的是好奇而非实际问题。
Java SE 7提供了Oracle所称的“fork/join框架”。这是一种预计上优于其他方法的方式来调度任务到多个处理器。虽然我理解它应该如何工作,但我无法理解它为什么更优秀以及有关抢占式任务执行的声明。
也许有人对这种方法更有深入的见解(除了因为它有一个花哨的名字)。
fork/join的基本组件是ForkJoinTask
,它们是Future
,其思想是要么立即执行任务(“立即”这个词措辞不太准确,因为它暗示着任务会在主线程同步执行,事实上它是在Future
内部执行),直到达到某个阈值或者将任务递归地分成两个任务,直到达到阈值。
Future
是一个将异步运行的任务封装成一个对象的概念,具体实现方式是不透明和未指定的。你有一个函数可以验证结果是否可用,并且你可以得到一个函数来(等待并)检索结果。
严格来说,你甚至不知道Future
是否异步运行,它可能在get()
内执行。在理论上,实现也可以为每个Future
创建一个线程或使用线程池。
实际上,Java将Future
实现为任务队列上的任务,并附加了一个线程池(整个fork/join框架也是如此)。
fork/join文档给出了以下具体示例:
protected void compute() {
if (mLength < sThreshold) {
computeDirectly();
return;
}
int split = mLength / 2;
invokeAll(new ForkBlur(mSource, mStart, split, mDestination),
new ForkBlur(mSource, mStart + split, mLength - split,
mDestination));
}
这将以与归并排序相同的方式遍历任务,并将它们提交到底层线程池的任务队列中(感谢递归)。
例如,假设我们有一个包含32个“项”的数组要处理,并且阈值为4,平均分割,则会产生8个任务,每个任务包含4个“项”,并且看起来像这样:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
.
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
. . .
00 01 02 03 04 05 06 07|08 09 10 11 12 13 14 15|16 17 18 19 20 21 22 23|24 25 26 27 28 29 30 31
. . . . . . .
00 01 02 03|04 05 06 07|08 09 10 11|12 13 14 15|16 17 18 19|20 21 22 23|24 25 26 27|28 29 30 31
------------------------------------------------------------------------------------------------
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
在单核处理器上,这将以非常复杂的方式按顺序提交/执行1-2-3-4-5-6-7-8任务组。在双核处理器上,这将提交/执行(1,3)-(2,4)-(5,7)-(6,8)。在四核处理器上,这将提交/执行(1,3,5,7)-(2,4,6,8)。相比之下,一个没有所有优秀特性的"天真"实现会立即将任务1-2-3-4-5-6-7-8提交到任务队列中。
在单核处理器上,这将提交/执行1-2-3-4-5-6-7-8。在双核处理器上,这将提交/执行(1,2)-(3,4)-(5,6)-(7,8)。在四核处理器上,这将提交/执行(1,2,3,4)-(5,6,7,8)。
问题:
1.为什么不是简单地将sThreshold连续的项塞入一个任务中,并将一个任务接一个任务提交到线程池的任务队列中,而是生成了一个类似树形递归层次结构?这涉及构建、引用和销毁N + log2(N)个实际上什么都不做的子任务对象。这种方法为什么更优越?
2.没有保留参考位置的本地性,既不缓存处理器,也不虚拟内存。这种方法为什么更优越?
3.除了在单处理器系统上,任务保证不会按其原始顺序的任何地方接近的顺序进行调度。如果确实没有关系,那可能就没有问题,但是这使得像围栏或障碍物之类的事情几乎不可行。唯一的方式是等待根对象完成,并且只在此之后提交新任务。这相当于完全的管道停顿(这正是您永远不想发生的)。
4.Oracle文档声称,这种方法实现了工作窃取,并因此比线程池更好。我没有看到这种情况发生。我能看到的只是以非常复杂的方式将任务提交到一个普通的线程池。这个方法是如何神奇地实现工作偷取的?