简体   繁体   English

Java 并行流未按预期工作

[英]Java parallel stream not working as expected

I have the following code:我有以下代码:

Map<String, Person> targetPerson = targetPersonList
                                     .stream()
                                     .collect(toMap(Person::getKey,  Function.identity()));

where targetPersonList is quite a large list , and the above code takes like 38 minutes to complete.其中 targetPersonList 是一个相当大的列表,上面的代码需要 38 分钟才能完成。 So I thought the following code should speed it up a little bit所以我认为下面的代码应该加快一点速度

Map<String, Person> targetPerson = targetPersonList
                                     .parallelStream()
                                     .collect(toMap(Person::getKey, Function.identity()));

It's actually the opposite, the 'parallel' piece , takes 1 hour and 20 minutes.它实际上是相反的,“平行”部分,需要 1 小时 20 分钟。 I have a Core i7 8th generation, which should have 6 cores and 12 threads, what is the problem then?我有一个酷睿 i7 8 代,应该有 6 个内核和 12 个线程,那有什么问题? Is there something fundamentally wrong with my understanding of parallel streams ?我对并行流的理解有什么根本性的错误吗?

Needing 38 minutes just to fill a HashMap is an unusual long time.仅填充HashMap就需要 38 分钟,这是一段不寻常的长时间。 It suggests that either, Person::getKey is performing an expensive construction or the result is an object with a less than optimal hashCode or equals implementation.它表明Person::getKey正在执行一个昂贵的构造,或者结果是一个对象的hashCodeequals实现不是最优的。

On my machine, filling a map with ten million elements with a reasonable hashCode or equals implementation takes less than a second, hundred millions still only need a few seconds and then, the memory consumption becomes a problem.在我的机器上,用合理的hashCodeequals实现填充一千万个元素的地图需要不到一秒钟的时间,亿万仍然只需要几秒钟,然后,内存消耗成为一个问题。

That said, the worse performance of the parallel stream doesn't come at a surprise.也就是说,并行流的较差性能并不令人意外。 As discussed in “ Should I always use a parallel stream when possible?正如“我应该尽可能使用并行流吗? ”, the parallel processing has some fixed overhead and you need some significant (per element) workload to have a benefit greater than the overhead. ”,并行处理有一些固定的开销,您需要一些重要的(每个元素)工作负载才能获得比开销更大的收益。

In your specific example, there's no benefit at all.在您的具体示例中,根本没有任何好处。

The parallel collect operation works by splitting the stream elements into chunks, to be processed by different worker threads.并行collect操作的工作原理是将流元素分成块,由不同的工作线程处理。 Each of them will create a new local container, in case of toMap a map of the same type as the end result, then, each thread will accumulate elements into its local container, ie put values into the map, and when two worker threads have finished their work, the partial results will be merged, which implies putting all elements of one map into the other.他们每个人都会创建一个新的本地容器,在toMap的情况下是一个与最终结果相同类型的映射,然后,每个线程将元素累积到其本地容器中,即将值放入映射中,当两个工作线程有完成他们的工作,部分结果将被合并,这意味着将一张地图的所有元素放入另一张地图。

Since you have no filtering operation and the absence of a merge function implies that all keys are unique, it's easy to conclude that in the best case you have two worker threads filling two maps of the same size perfectly in parallel, followed by putting one of these maps into the other, taking as much time as has been saved by the previous parallel processing.由于您没有过滤操作并且没有合并函数意味着所有键都是唯一的,因此很容易得出结论,在最好的情况下,您有两个工作线程完美地并行填充两个相同大小的映射,然后放置其中一个这些映射到另一个中,花费的时间与先前并行处理节省的时间一样多。

Your example also doesn't include potentially expensive intermediate operations, so only if Person::getKey is expensive, it's costs could be reduced by the parallel processing.您的示例也不包括可能昂贵的中间操作,因此只有当Person::getKey很昂贵时,并行处理才能降低其成本。

As discussed in this answer , using toConcurrentMap instead of toMap can improve such a scenario, as it allows to skip the merge operation and having all-unique keys implies very low contention when all worker threads put into one map.正如这个答案中所讨论的,使用toConcurrentMap而不是toMap可以改善这种情况,因为它允许跳过合并操作,并且当所有工作线程都放入一个映射时,具有唯一键意味着非常低的争用。

However, it's worth investigating the actual cause for the performance problem.但是,值得调查性能问题的实际原因。 When the issue is the hashCode or equals implementation of the key object, fixing it will gain far more.当问题是关键对象的hashCodeequals实现时,修复它会获得更多收益。 Also, concurrency can't solve problems related to an almost full heap.此外,并发不能解决与几乎满堆相关的问题。

Finally, toConcurrentMap returns a concurrent map, which may impose higher costs on subsequent processing, even when you don't intent to use this map with multiple threads.最后, toConcurrentMap返回一个并发映射,这可能会给后续处理带来更高的成本,即使您不打算将此映射用于多个线程。

It's beneficial to use a parallel stream if you have a heavy operator.如果您有一个繁重的运算符,使用并行流是有益的。 For example, a long-executing map function.例如,一个长时间执行的映射函数。 In your case, it's better not to use streams as well, because it will only slow it down.在您的情况下,最好也不要使用流,因为它只会减慢速度。

However, I have a set of advice.不过,我有一套建议。

  1. Since you are probably want to use HashMap, make sure you implement and cache the result of the hashCode() function.由于您可能想要使用 HashMap,因此请确保实现并缓存hashCode()函数的结果。
  2. Initialize your map with constructor passing initial capacity new HashMap<>(targetPersonList.size());使用传递初始容量的构造函数初始化您的地图new HashMap<>(targetPersonList.size());
  3. Use for-loop and insert every element if all of the values are pre-calculated.如果所有值都是预先计算好的,则使用 for 循环并插入每个元素。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM