Java有一种类似于线程构建块的并行任务框架,称为Fork-Join框架。它可用于当前的Java SE 6,并将包含在即将推出的Java SE 7中。
除了javadoc类文档外,还有可用于入门该框架的资源。从jsr166页面可以看到,还有一个维基百科,其中包含这些类的其他文档、注释、建议、示例等。
例如矩阵乘法的fork-join示例是一个很好的起点。
我在解决一些
Intel 2009年线程挑战赛时使用了fork-join框架。该框架轻量且低开销 - 我的Java作品是Kight's Tour问题中唯一的参赛作品,并且在比赛中胜出了其他参赛作品。Java源代码和文稿可从挑战网站下载。
编辑:
我不知道将类或代码片段推入池后它们会变成什么样子[...]
您可以通过对
ForKJoinTask子类之一进行子类化来创建自己的任务,例如
RecursiveTask。以下是如何并行计算斐波那契数列的方法(摘自
RecursiveTask
javadocs - 注释为我的)。
class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonnaci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.invoke() + f1.join();
}
}
然后运行此任务并获取结果
// default parallelism is number of cores
ForkJoinPool pool = new ForkJoinPool();
Fibonacci f = new Fibonacci(100);
int result = pool.invoke(f);
这只是一个简单的例子,以保持事情简单。实际上,性能不会那么好,因为与任务框架的开销相比,任务执行的工作微不足道。作为一个经验法则,任务应该执行一些重要的计算 - 足以使框架开销微不足道,但不要过多地让你最终只剩下一个核心运行一个大任务。将大型任务分成较小的任务可以确保一个核心不会在其他核心处于空闲状态时做大量的工作 - 使用较小的任务可以使更多的核心保持繁忙,但不要太小,以至于任务没有真正的工作。
“[...]或者当您需要复制所有内容并将所有内容推送到池中时,代码可能会看起来多么奇怪。”
只有任务本身被推入池中。理想情况下,您不希望复制任何内容:为了避免干扰和需要锁定,这会减慢程序的速度,您的任务应该使用独立数据进行操作。只读数据可以在所有任务之间共享,并且不需要复制。如果线程需要协作构建某些大型数据结构,则最好将它们分别构建,然后在最后将它们组合起来。组合可以作为单独的任务完成,或者每个任务都可以将其拼图片段添加到整体解决方案中。这通常需要某种形式的锁定,但如果任务的工作量比更新解决方案的工作量大得多,则这不是一个重要的性能问题。我的骑士巡游解决方案采用这种方法来更新棋盘上的公共旅游存储库。
处理任务和并发与常规单线程编程相比是一种范式转变。通常有多种设计可用来解决给定问题,但只有其中一些适合线程解决方案。需要尝试几次才能掌握如何将熟悉的问题重新构建为多线程方式。学习的最佳方法是查看示例,然后自己尝试。始终进行性能分析,并测量更改线程数量的影响。您可以在池构造函数中显式设置要使用的线程(核心)数。当任务被线性分解时,随着线程数量的增加,您可以期望近似线性的加速。