我试图理解我在Streams上的文档中发现的警告。我已经养成了使用forEach()作为通用迭代器的习惯。这导致我编写这种类型的代码:
public class FooCache {
private static Map<Integer, Integer> sortOrderCache = new ConcurrentHashMap<>();
private static Map<Integer, String> codeNameCache = new ConcurrentHashMap<>();
public static void populateCache() {
List<Foo> myThings = getThings();
myThings.forEach(thing -> {
sortOrderCache.put(thing.getId(), thing.getSortOrder());
codeNameCache.put(thing.getId(), thing.getCodeName())
});
}
}
这是一个微不足道的例子。我知道这段代码违反了Oracle针对有状态的lamdas和副作用的警告。但我不明白为什么会出现这种警告。
运行此代码时,它似乎表现得如预期。那么我该如何打破这个来证明为什么这是一个坏主意呢?
在排序中,我读到了这个:
如果并行执行,则ArrayList的非线程安全性会 导致不正确的结果,并添加所需的同步将导致 争论,破坏了并行性的好处。
但是,任何人都可以添加清晰度来帮助我理解警告吗?
答案 0 :(得分:3)
来自Javadoc:
还要注意尝试从行为中访问可变状态 参数在安全方面给你一个糟糕的选择 性能; 如果您不同步对该状态的访问权限,则为 数据竞争,因此你的代码被破坏,但如果你这样做 同步对该状态的访问,您可能会有争用破坏 你想要从中获益的并行性。最好的方法是 避免有状态的行为参数完全流动操作; 通常有一种方法可以重构流管道以避免 有状态。
这里的问题是,如果你访问一个可变状态,你就会在两边松动:
Stream
尝试最小化的同步ConcurrentHashMap
,则需要付费)。现在,在您的示例中,这里有几点:
Stream
和多线程流,则需要使用parralelStream()
中的myThings.parralelStream()
;就目前而言,forEach
提供的java.util.Collection
方法很简单for each
。HashMap
作为static
成员,并对其进行变更。 HashMap
不是线程安全的;您需要使用ConcurrentHashMap
。在lambda中,如果是Stream
,则不得改变流的来源:
myThings.stream().forEach(thing -> myThings.remove(thing));
这可能有效(但我怀疑它会抛出ConcurrentModificationException
)但这可能不起作用:
myThings.parallelStream().forEach(thing -> myThings.remove(thing));
那是因为ArrayList
不是线程安全的。
如果您使用同步视图(Collections.synchronizedList
),那么您将获得性能,因为您在每次访问时都会进行同步。
在您的示例中,您宁愿使用:
sortOrderCache = myThings.stream()
.collect(Collectors.groupingBy(
Thing::getId, Thing::getSortOrder);
codeNameCache= myThings.stream()
.collect(Collectors.groupingBy(
Thing::getId, Thing::getCodeName);
整理器(这里是groupingBy
)完成你正在做的工作并且可以按顺序调用(我的意思是,Stream可以分成几个线程,整理器可以多次调用(在不同的线程中) )然后它可能需要合并。
顺便说一下,您最终可能会删除codeNameCache
/ sortOrderCache
,只需存储id-&gt; Thing映射。
答案 1 :(得分:1)
我相信文档中提到了以下代码所示的副作用:
List<Integer> matched = new ArrayList<>();
List<Integer> elements = new ArrayList<>();
for(int i=0 ; i< 10000 ; i++) {
elements.add(i);
}
elements.parallelStream()
.forEach(e -> {
if(e >= 100) {
matched.add(e);
}
});
System.out.println(matched.size());
此代码并行流式传输列表,并尝试在符合特定条件的情况下将元素添加到其他列表中。由于结果列表未同步,因此在执行上述代码时将获得java.lang.ArrayIndexOutOfBoundsException
。
修复方法是创建一个新列表并返回,例如:
List<Integer> elements = new ArrayList<>();
for(int i=0 ; i< 10000 ; i++) {
elements.add(i);
}
List<Integer> matched = elements.parallelStream()
.filter(e -> e >= 100)
.collect(Collectors.toList());
System.out.println(matched.size());
答案 2 :(得分:0)
副作用经常对状态和背景做出假设。同时,您无法保证您看到元素的特定顺序,并且多个线程可能同时运行。
除非你为此编码,否则这会产生非常微妙的错误,在尝试并行时很难跟踪和修复。