我有一个像这样的测试代码:
List<Integer> list = new ArrayList<>(1000000);
for(int i=0;i<1000000;i++){
list.add(i);
}
List<String> values = new ArrayList<>(1000000);
list.stream().forEach(
i->values.add(new Date().toString())
);
System.out.println(values.size());
运行它,我得到了正确的输出:1000000。
但是,如果我将stream()
更改为parallelStream()
,则为:
list.parallelStream().forEach(
i->values.add(new Date().toString())
);
我得到了一个随机输出,例如:920821。
怎么了?
答案 0 :(得分:10)
ArrayList
未同步。未定义尝试同时向其添加元素。来自forEach
:
对于并行流管道,此操作不保证遵守流的遭遇顺序,因为这样做会牺牲并行性的好处。 对于任何给定元素,可以在任何时间以及库选择的任何线程中执行操作。
在第二个示例中,您最终会同时在阵列列表上调用add
多个线程,ArrayList
文档说明:
请注意,此实现未同步。如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改了列表,则必须在外部进行同步。
如果您将ArrayList
的使用更改为Vector
,您将获得正确的结果,因为此列表实现已同步。它的Javadoc说:
与新的集合实现不同,
Vector
已同步。
但是,do not use it!此外,由于显式同步,它可能最终会变慢。
明确地避免Stream API使用mutable reduction方法提供collect
范例的情况。以下
List<String> values = list.stream().map(i -> "foo").collect(Collectors.toList());
将始终提供正确的结果,无论是否并行运行。 Stream管道在内部处理并发和guarantees that it is safe to use a non-concurrent collector in a collect operation of a parallel stream。 Collectors.toList()
是一个内置的收集器,将Stream的元素累积到列表中。
答案 1 :(得分:4)
使用消费者,您必须担心线程安全。一个更简单的解决方案,让Stream API累积结果。
List<String> values = IntStream.range(0, 1_000_000).parallel()
.mapToObj(i -> new Date().toString())
.collect(Collectors.toList());
避免使用像Vector这样的线程安全收集器的一个关键原因是它需要每个线程获取共享锁是一个瓶颈,即你将花时间获取和释放锁,并且一次只有一个线程可以访问它。您可以轻松地获得比单独使用一个线程更慢的解决方案。
答案 2 :(得分:2)
values.add(String)
不是线程安全的。当您从不同的线程调用此方法而不进行同步时,无法保证它将按预期工作。
要解决此问题,您可以:
Vector
或CopyOnWriteArrayLis
。synchronize(this){values.add(new Date().toString())}
放入代码中。注意i->
在同步块IntStream.range(0, 1_000_000).parallel().mapToObj(i -> new Date().toString()).collect(Collectors.toList());