Java 8 Streams中副作用的危险是什么?

时间:2017-10-31 17:22:51

标签: java java-stream

我试图理解我在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的非线程安全性会   导致不正确的结果,并添加所需的同步将导致   争论,破坏了并行性的好处。

但是,任何人都可以添加清晰度来帮助我理解警告吗?

3 个答案:

答案 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)

副作用经常对状态和背景做出假设。同时,您无法保证您看到元素的特定顺序,并且多个线程可能同时运行。

除非你为此编码,否则这会产生非常微妙的错误,在尝试并行时很难跟踪和修复。