1 Parallel 入门
public static void main(String[] args) {
long sum = 0L;
for (long l = 0; l < 10_000_000; l++) {
sum += l;
};
}
最简单传统的写法就是上面这样
我们想要使其基于并行的方式运算将是非常困难的,最起码在JDK 1.7版本以前,我们需要很好地处理数据的分区,需要为每一个分区分配不同的线程,并且对线程进行管理,然后还要处理资源竞争的情况,以及最后等待不同线程任务的结束和汇总最终的结果。
由于Fork Join计算框架的引入,我们可以通过划分不同的RecursiveTask来处理对应的数据分区,但是仍然需要我们根据对应的逻辑,显式地对数据元素进行子任务拆分
参考:ForkJoinPool,这里我已经使用Fork Join实现了一个累加
现在使用并行流Parallel Stream来处理
public void parallel() {
LongStream.range(0, 10_000_000)
.parallel()
.reduce(0L, Long::sum);
}
2 Spliterator 接口
Spliterator也是Java 8引入的一个新的接口,其主要应用于Stream中,尤其是在并行流进行元素块拆分时主要依赖于Spliterator的方法定义,这与我们在ForkJoinPool中进行子任务拆分是一样的,只不过对Spliterator的引入将任务拆分进行了抽象和提取
2.1 Spliterator接口方法详解
/**
该接口非常类似于迭代器方法,其主要作用是对Stream中的每一个元素进行迭代,
并且交由Consumer进行处理,若返回布尔值true则代表着当前Stream还有元素,若返回false则表明没有元素。
**/
boolean tryAdvance(Consumer<? super T> action)
/**
该接口方法代表着对当前Stream中的元素进行分区,派生出另外的Spliterator以供并行操作,
若返回值为null,则代表着不再派生出新的分区,这一点非常类似于Fork Join中的子任务拆分操作。
**/
trySplit()
/**
该方法主要用于评估当前Stream中还有多少元素未被处理,
一般进行子任务划分时会将基于该接口方法的返回值作为主要依据。
**/
estimateSize()
/**
与Collector的特征值接口类似,
该方法主要用于定义当前Spliterator接口的特征值,其包含如下几个值可用于定义。
· SIZED - 能够准确地评估出当前元素的数量。
· SORTED - 数据源是已排序的元素。
· SUBSIZED - 利用trySplit()方法进行子任务拆分后,Spliterator元素可被准确评估。
· CONCURRENT - 数据源可被线程安全地修改。
· DISTINCT - 数据源中的数量是去重的,可以根据equalTo方法进行判断。
· IMMUTABLE - 数据源元素是不会被修改的,比如add、remove等。
· NONNULL - 数据源的每一个元素都非空。
· ORDERED -数据源是有序的元素。
**/
characteristics()
在Java 8中,所有的容器类都增加了对Spliterator的支持,我们可以通过方法直接获取Spliterator。
List<String> list = new ArrayList<>();
// 获取Spliterator
Spliterator<String> spliterator = list.spliterator();
int expected = Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
// 断言该Spliterator的特征值
assert expected == spliterator.characteristics();
2.2 自定义Spliterator及Stream
通过自定义的方式来加深体会Spliterator的原理
public class MySpliterator<T> implements Spliterator<T> {
private final T[] elements;
private int currentIndex = 0;
private final int CAPACITY;
public MySpliterator(T[] elements) {
this.elements = elements;
CAPACITY = elements.length;
}
@Override
public boolean tryAdvance(Consumer<? super T> action) {
// 处理元素
action.accept(elements[currentIndex++]);
// 判断Stream中的元素是否已排干
return currentIndex < CAPACITY;
}
/**
* 拆分
*
* @return
*/
@Override
public Spliterator<T> trySplit() {
int remainingSize = CAPACITY - currentIndex;
// 以10作为基准进行子任务拆分,若当前残留元素数量少于10,则不再拆分
if (remainingSize < 10) {
return null;
}
// 拆分的过程,进行数组拷贝,并且返回一个新的Spliterator
int middleSize = (remainingSize) / 2;
T[] newElements = (T[]) new Object[middleSize];
System.arraycopy(elements, currentIndex, newElements, 0, middleSize);
final MySpliterator<T> spliterator = new MySpliterator<>(newElements);
this.currentIndex = currentIndex + middleSize;
return spliterator;
}
@Override
public long estimateSize() {
// 由于数组是确定的,因此可以非常精准地得出Stream中的残留元素
return CAPACITY - currentIndex;
}
@Override
public int characteristics() {
// 有序、数量固定、子任务数量也固定,担保不存在非空值,并且不允许改变源
return Spliterator.ORDERED | Spliterator.SIZED
| Spliterator.SUBSIZED | Spliterator.NONNULL
| Spliterator.IMMUTABLE;
}
}
测试拆分
public static void main(String[] args) {
// 定义一个数组,有30个元素。
Integer[] ints = new Integer[]{1, 2, 3, 4, 5,
6, 7, 8, 9, 10,
11, 12, 13, 14, 15,
16, 17, 18, 19, 20,
21, 22, 23, 24, 25,
26, 27, 28, 29, 30
};
// 定义我们自定义的Spliterator并且传入数组
MySpliterator<Integer> mySpliterator = new MySpliterator<>(ints);
// 调用拆分方法,拆分后s1将被分配1~15之间的元素
Spliterator s1 = mySpliterator.trySplit();
// 此刻mySpliterator 的元素为16~30之间的元素
// 再次调用拆分方法,s2将被分配16~22之间的元素,与此同时,mySpliterator 将保留其余的元素
Spliterator s2 = mySpliterator.trySplit();
// 输出s1中的元素
s1.forEachRemaining(System.out::println);
System.out.println("==================");
// 输出s2中的元素
s2.forEachRemaining(System.out::println);
System.out.println("==================");
// 输出mySpliterator 中的元素
mySpliterator.forEachRemaining(System.out::println);
}
将会得到如下的输出,输出结果与我们在代码注释中的分析完全一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
==================
16
17
18
19
20
21
22
==================
23
24
25
26
27
28
29
30
Spliterator已经创建完成,想要使其能够应用于Stream之中,还需要基于该Spliterator创建一个全新的Stream,创建方式很简单使用StreamSupport提供的方法即可
- 串行
// false代表串行
Stream<Integer> stream = StreamSupport.stream(mySpliterator, false);
// 通过reduce操作对Stream中的元素进行求和
int sum = stream.reduce(0, Integer::sum);
// 断言,与ints之和进行对比,检验自定义的Stream及Spliterator是否存在问题
assert sum == Stream.of(ints).reduce(0, Integer::sum);
// 验证通过
// true代表并行
Stream<Integer> stream = StreamSupport.stream(mySpliterator, true);
Stream<Integer> stream = StreamSupport.stream(mySpliterator, true);
// 通过reduce操作对Stream中的元素进行求和运算
int sum = stream.reduce(0, Integer::sum);
// 断言,与ints之和进行对比,检验我们自定义的Stream及Spliterator是否存在问题
assert sum == Stream.of(ints).reduce(0, Integer::sum);
// 验证通过