在尝试扩展代码之前,您应该首先了解几个基本错误。
首先,forEach 不保证元素处理的特定顺序,因此添加到List 可能是错误的工具,即使对于顺序流也是如此,但是,与 parallel 流添加到像 LinkedList 这样不是线程安全的集合中,因为该操作将同时执行。
但是即使resourceMemory 是一个线程安全的集合,您的代码仍然被破坏,因为您的filter 条件和终端操作之间存在干扰。 .filter(o -> !resourceMemory.contains(o)) 查询您在终端操作中修改的同一个列表,不难理解即使使用线程安全集合,它也会停止:
两个或多个线程可能会处理过滤器并发现该元素不包含在列表中,然后它们都会添加该元素,这与您不重复的明显意图相矛盾。
您可以求助于forEachOrdered,它将按顺序和非并发地执行操作:
body.getSurroundings().parallelStream()
.filter(o -> o instanceof ResourcePoint)
.map(o -> (ResourcePoint)o)
.forEachOrdered(o -> {// not recommended, just for explanation
if(!resourceMemory.contains(o))
resourceMemory.add(o);
});
这会起作用,很明显你可以如何在该操作中添加到另一个列表,但它与推荐的编码风格相去甚远。此外,此终端操作与所有处理线程同步这一事实将破坏并行处理的任何潜在好处,特别是当此流管道最昂贵的操作是在 LinkedList 上调用 contains 时(必须) 发生在单线程中。
将流元素收集到列表中的正确方法是通过,顾名思义,collect:
List<ResourcePoint> resourceMemory
=body.getSurroundings().parallelStream()
.filter(o -> o instanceof ResourcePoint)
.map(o -> (ResourcePoint)o)
.distinct() // no duplicates
.collect(Collectors.toList()); // collect into a list
这不会返回LinkedList,但您应该仔细考虑是否真的需要LinkedList。在 99% 的情况下,你没有。如果您真的需要LinkedList,可以将Collectors.toList() 替换为Collectors.toCollection(LinkedList::new)。
现在,如果您确实必须添加到在您的控件之外创建的现有列表中,该列表可能已经包含元素,您应该考虑上面提到的事实,即您必须确保对非线程安全列表的单线程访问无论如何,所以从并行流中执行它根本没有任何好处。在大多数情况下,让流独立于该列表工作并随后在单个线程步骤中添加结果会更有效:
Set<ResourcePoint> newElements=
body.getSurroundings().parallelStream()
.filter(o -> o instanceof ResourcePoint)
.map(o -> (ResourcePoint)o)
.collect(Collectors.toCollection(LinkedHashSet::new));
newElements.removeAll(resourceMemory);
resourceMemory.addAll(newElements);
在这里,我们收集到一个LinkedHashSet,这意味着维护遭遇顺序并整理出新元素中的重复项,然后在新元素上使用removeAll删除目标列表中的现有元素(这里我们受益于临时集合的哈希集性质),最后,新元素被添加到目标列表中,正如解释的那样,对于非线程安全的目标集合,无论如何都必须单线程发生。
使用此解决方案很容易将newElements 添加到另一个目标集合,比编写自定义收集器在流处理期间生成两个列表要容易得多。但请注意,上面写的流操作太便宜了,无法从并行处理中获得任何好处。您将需要大量元素来补偿初始多线程开销。甚至有可能没有任何数字可以带来回报。