在Java 7之前,并行处理数据集合非常麻烦
- 第一,你得明确地把包含数据的数据结构分成若干子部分
- 第二,你要给每个子部分分配一个独立的线程
- 第三,你需要在恰当的时候 对它们进行同步来避免不希望出现的竞争条件
- 等待所有线程完成,最后把这些部分结果合并起 来
Java 7引入了一个叫作分支/合并的框架,让这些操作更稳定、更不易出错
Strean流将外部迭代转换为内部迭代,它允许声明性地将顺序流转换为并行流,在幕后使用分支合并框架能透明地进行并行处理
1. 使用并行流
并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷 分配给多核处理器的所有内核,让它们都忙起来
获取一个并行流是非常简单的,有如下两种方法
① 只需要对流调用一下parallel()就可以获取到一个并行流了
list.stream().parallel()
对顺序流调用parallel方法并不意味着流本身有任何实际的变化。它在内部实际上就是设了一个boolean标志,表示你想让调用parallel之后进行的所有操作都并行执行
同样,只需要对并行流调用sequential方法就可以把它变成顺序流
② 我们可以使用Collection接口提供给我们parallelStream()获取到一个并行流
list.parallelStream()
并行流使用的线程池
并行流内部使用了默认的ForkJoinPool,它默认的 线程数量就是你的处理器数量
2. 分支/合并框架
并行流幕后使用的是分支合并框架,把内容分成多个数据块,并用不同的线程分别处理每个数据块的流
分支/合并框架的目的是以递归方式将可以并行的任务拆分成更小的任务,然后将每个子任 务的结果合并起来生成整体结果
它是ExecutorService接口的一个实现,它把子任务分配给线程池(称为ForkJoinPool)中的工作线程
2.1 使用RecursiveTask
要把任务提交到这个池,必须创建RecursiveTask的一个子类,其中R是并行化任务(以 及所有子任务)产生的结果类型,或者如果任务不返回结果,则是RecursiveAction类型(当 然它可能会更新其他非局部机构)
要定义RecursiveTask,只需实现它唯一的抽象方法 compute
这个方法同时定义了将任务拆分成子任务的逻辑,以及无法再拆分或不方便再拆分时,生成 单个子任务结果的逻辑
实际上它并行处理的逻辑就是分治算法的并行版本,将任务分为多个子任务并行处理最后再合并所有任务的结果
所有compute的逻辑如下
if (任务足够小或不可分) {
顺序计算该任务
} else {
将任务分成两个子任务
递归调用本方法,拆分每个子任务,等待所有子任务完成
合并每个子任务的结果
}
public static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
return new ForkJoinPool().invoke(task);
}
2.2 工作窃取
对于分支合并并行处理的方法来说最好能划分子任务让所有的处理器都处于繁忙,但实际中,每 个子任务所花的时间可能天差地别,要么是因为划分策略效率低,要么是有不可预知的原因,比如 磁盘访问慢,或是需要和外部服务协调执行
分支/合并框架工程用一种称为工作窃取(work stealing)的技术来解决这个问题。在实际应 用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分 配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执 行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经 空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队 列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队 列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程 之间平衡负载
2.3 Spliterator
在上面我们明确地指定了如何将任务拆分为子任务的逻辑,但是在使用并行流地时候我们并没有明确指定如何拆分任务,这是因为有Spliterator这种机制来自动拆分任务
Spliterator是Java 8中加入的另一个新接口;这个名字代表“可分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行 而设计的
Java 8已经为集合框架中包含的所有数据结构提供了一个 默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。 这个接口定义了若干方法
public interface Spliterator<T> {
//tryAdvance方法的行为类似于普通的 Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true
boolean tryAdvance(Consumer<? super T> action);
//但 trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分 给第二个Spliterator(由该方法返回),让它们两个并行处
Spliterator<T> trySplit();
//可通过 estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值 也有助于让拆分均匀一点
long estimateSize();
int characteristics();
}
拆分过程
将Stream拆分成多个部分的算法是一个递归过程,第一步是对第一个 Spliterator调用trySplit,生成第二个Spliterator。第二步对这两个Spliterator调用 trysplit,这样总共就有了四个Spliterator。这个框架不断对Spliterator调用trySplit 直到它返回null,表明它处理的数据结构不能再分割
3. 高效使用并行流
要考虑流背后的数据结构是否易于分解。例如,ArrayList的拆分效率比LinkedList 高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历