使用parallelstream()在Java 8中填充Map是否安全

One*_*ror 11 java java-8

我有一个包含100万个对象的列表,我需要将其填充到Map中.现在,我想减少将其填充到Map中的时间,为此我计划使用Java 8 parallelstream(),如下所示:

List<Person> list = new LinkedList<>();
Map<String, String> map = new HashMap<>();
list.parallelStream().forEach(person ->{
    map.put(person.getName(), person.getAge());
});
Run Code Online (Sandbox Code Playgroud)

我想问一下,通过并行线程填充这样的Map是否安全.难道不可能出现并发问题,并且某些数据可能会在Map中丢失吗?

Tun*_*aki 18

parallelStream()用来收集到一个非常安全HashMap.但是,使用它是不安全的parallelStream(),forEach并且消费者会添加东西HashMap.

HashMap不是同步类,并且尝试同时将元素放入其中将无法正常工作.这是forEach将要做的,它将调用给定的使用者,它HashMap可以同时从多个线程将元素放入.如果你想要一个简单的代码来证明这个问题:

List<Integer> list = IntStream.range(0, 10000).boxed().collect(Collectors.toList());
Map<Integer, Integer> map = new HashMap<>();
list.parallelStream().forEach(i -> {
    map.put(i, i);
});
System.out.println(list.size());
System.out.println(map.size());
Run Code Online (Sandbox Code Playgroud)

一定要运行几次.操作后打印的地图大小不是10000,这是列表的大小,但稍微少一点,这是一个非常好的机会(并发的乐趣).

这里的解决方案一如既往不使用forEach,而是使用方法和内置的可变缩减方法:collecttoMap

Map<Integer, Integer> map = list.parallelStream().collect(Collectors.toMap(i -> i, i -> i));
Run Code Online (Sandbox Code Playgroud)

使用在上面的示例代码行的代码,你可以放心,地图大小将始终是10000的流API确保它是安全的,收集到非线程安全的容器,即使是在平行.这也意味着你不需要使用toConcurrentMap是安全的,如果你特别想要一个ConcurrentMap结果而不是一般的,那么需要这个收集器Map; 但就线程安全而言collect,您可以同时使用两者.


Boh*_*ian 7

HashMap不是线程安全的,而是ConcurrentHashMap;改用那个

Map<String, String> map = new ConcurrentHashMap<>();
Run Code Online (Sandbox Code Playgroud)

并且您的代码将按预期工作。


forEach()vs 的性能比较toMap()

在 JVM 预热后,使用 1M 元素、使用并行流和使用中值计时,该forEach()版本始终比toMap()版本快 2-3 倍。

结果在完全唯一、25% 重复和 100% 重复输入之间是一致的。

  • 因为我在收集之前用一个简单的方法做了第二个基准测试,做一些工作(输入整数的字符串操作等等,试图欺骗 JIT,可能失败了,但是呃),然后 `collect(toMap())` 随后变成了比使用 `forEach` 方法更快。无论如何,我认为可以公平地说,如果没有确切的完整管道进行测试,它并不是真正的定论。(在最近的 Window 10 x64 上使用 JDK 1.8.0_102 运行所有这些)。 (3认同)
  • 当您打印性能比较时,您还应该发布您正在比较的*内容*。最值得注意的是,与“25% 重复”一起使用的普通 `toMap` 会失败并抛出异常,而不是产生可比较的结果。这表明您使用了未指定的合并函数,这显然不是 `forEach` 方法所做的。此外,`toMap` 和 `toConcurrentMap` 之间有一个根本的区别...... (2认同)
  • @Bohemian:仍然,这意味着使用`Map.merge` 而不是`Map.put`,这会有所不同。此外,`forEach` 是一个无序操作,因此您可以使用`.unordered().collect(toMap(…))` 来实现类似的效果。但是,如上所述,`toMap` 与 `toConcurrentMap` 是一个根本不同的操作。如果你没有任何与性能相关的上游操作,但仍然想要并行操作,`toConcurrentMap` 是更好的选择(很像 `forEach` 到 `ConcurrentMap` 方法),尽管很可能,单线程操作会在这种情况下效率更高。 (2认同)
  • @assylias:`toMap` 与`toList` 类似,永远不会从并行操作中受益,因为合并成本与先前并行处理的任何潜在收益一样高。它们仅在并行流中有用,当上游操作受益于无争用并行处理时。对于仅包含收集操作而没有任何有用的流处理的基准测试,它们总是会失败。 (2认同)
  • @Bohemian 确保在基准测试中包含简单的 for 循环变体作为基准,因为使用“空”管道(仅将整数列表收集到地图中)制作的基准测试,for 循环更快比所有其他解决方案的总和(事实上,`forEach` + `ConcurrentMap` 也比 `collect(toMap())` 快一点)。这实际上取决于收集之前发生的操作。 (2认同)