Java8 parallel 遇到的坑

什么是Stream

Java8 引入的stream接口可以说一大亮点,对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。

我遇到的问题

直接上代码吧

				try {
					Stream<ServiceReference<FileMigrator>> migratorStream = fileMigrators.stream();

					migratorStream.map(
						_context::getService
					).parallel(
					).forEachOrdered(
						fm -> {
							List<Problem> fileProlbems = fm.analyze(file);

							if (ListUtil.isNotEmpty(fileProlbems)) {
								problems.addAll(fileProlbems);
							}
						}
					);
				}
				catch (Exception e) {
				}

这是一个利用并行,多个service对文件进行检查的一个测试的一部分。我发现在执行的过程中,发现的错误的数量是不稳定的,排除了代码的逻辑问题,因为我并没有对文件进行修改操作,所以不存在因为文件改变找不到错误点的问题。剩下的就是 我用了 parallel 并行的问题。注意并行不同于并发 可以参考一下这个博客

验证parallelStream是不是线程安全的

一个简单的例子,在下面的代码中采用stream的forEach接口对1-10000进行遍历,分别插入到3个ArrayList中。其中对第一个list的插入采用串行遍历,第二个使用paralleStream,第三个使用paralleStream的同时用ReentryLock对插入列表操作进行同步:

private static List<Integer> list1 = new ArrayList<>();
private static List<Integer> list2 = new ArrayList<>();
private static List<Integer> list3 = new ArrayList<>();
private static Lock lock = new ReentrantLock();

public static void main(String[] args) {
    IntStream.range(0, 10000).forEach(list1::add);

    IntStream.range(0, 10000).parallel().forEach(list2::add);

    IntStream.range(0, 10000).forEach(i -> {
    lock.lock();
    try {
        list3.add(i);
    }finally {
        lock.unlock();
    }
    });

    System.out.println("串行执行的大小:" + list1.size());
    System.out.println("并行执行的大小:" + list2.size());
    System.out.println("加锁并行执行的大小:" + list3.size());
}
串行执行的大小:10000
并行执行的大小:9595
加锁并行执行的大小:10000

并且每次的结果中并行执行的大小不一致,而串行和加锁后的结果一直都是正确结果。显而易见,stream.parallel.forEach()中执行的操作并非线程安全。

我用的一些方法进行弥补

  1. 直接加锁
				try {
					Stream<ServiceReference<FileMigrator>> migratorStream = fileMigrators.stream();

					migratorStream.map(
						_context::getService
					).parallel(
					).forEach(
						fm -> {
							lock.lock();
							try {
								List<Problem> fileProlbems = fm.analyze(file);

								if (ListUtil.isNotEmpty(fileProlbems)) {
									problems.addAll(fileProlbems);
								}
							}
							finally {
								lock.unlock();
							}
						}
					);
				}
				catch (Exception e) {
				}

经过多次测试,结果是正确的,得到的错误数量开始稳定了。方法可行,但是代码不优雅,加个lock,总是感觉有些突兀。

  1. 用synchronized
				List<ServiceReference<FileMigrator>> fileMigrators = Collections.synchronizedList(new ArrayList<ServiceReference<FileMigrator>>)

				.......

				try {
					Stream<ServiceReference<FileMigrator>> migratorStream = fileMigrators.stream();

					migratorStream.map(
						_context::getService
					).parallel(
					).forEachOrdered(
						fm -> {
							List<Problem> fileProlbems = fm.analyze(file);

							if (ListUtil.isNotEmpty(fileProlbems)) {
								problems.addAll(fileProlbems);
							}
						}
					);
				}
				catch (Exception e) {
				}

用这个方法,错误数量还是不太稳定,我会继续研究一下这块的问题。

  1. 用forEachOrdered 代替forEach(我推荐的)
				try {
					Stream<ServiceReference<FileMigrator>> migratorStream = fileMigrators.stream();

					migratorStream.map(
						_context::getService
					).parallel(
					).forEachOrdered(
						fm -> {
							List<Problem> fileProlbems = fm.analyze(file);

							if (ListUtil.isNotEmpty(fileProlbems)) {
								problems.addAll(fileProlbems);
							}
						}
					);
				}
				catch (Exception e) {
				}

这个是让parallel本来无须变得有序,可能会影响一点并行的效率,但是相对于单行的效率还是不可同年而语的。如果对于效率不是特别苛刻,我觉得本方法完全可行。
大家可以参考 这个链接

总结

parallelStream是线程不安全的,但不是不可避免的,可能还有很多比这些好很多的办法,希望大神能多多指点,踊跃留言,谢谢!

猜你喜欢

转载自blog.csdn.net/qq_24505155/article/details/85119267