Java 为什么共享可变性不好?

Java 为什么共享可变性不好?,java,java-8,java-stream,immutability,Java,Java 8,Java Stream,Immutability,我在看一个关于Java的演示,有一次,讲师说: “易变性没问题,共享很好,共享易变性是魔鬼的工作。” 他指的是以下代码,他认为这是一个“极坏的习惯”: //将偶数值加倍并将其放入列表中。 列表编号=数组.asList(1,2,3,4,5,1,2,3,4,5); List doubleOfEven=new ArrayList(); numbers.stream() .filter(e->e%2==0) .地图(e->e*2) .forEach(e->doubleOfEven.add(e)); 然

我在看一个关于Java的演示,有一次,讲师说:

“易变性没问题,共享很好,共享易变性是魔鬼的工作。”

他指的是以下代码,他认为这是一个“极坏的习惯”:

//将偶数值加倍并将其放入列表中。
列表编号=数组.asList(1,2,3,4,5,1,2,3,4,5);
List doubleOfEven=new ArrayList();
numbers.stream()
.filter(e->e%2==0)
.地图(e->e*2)
.forEach(e->doubleOfEven.add(e));
然后,他继续编写应该使用的代码,即:

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList());
列出两个事件2=
numbers.stream()
.filter(e->e%2==0)
.地图(e->e*2)
.collect(toList());
我不明白为什么第一段代码是“坏习惯”。对我来说,它们都达到了相同的目标。

对第一个示例片段的解释 这个问题在执行并行处理时起作用

不必要地使用了副作用,当并行执行时,
ArrayList的非线程安全性将导致不正确的结果

不久前,我读了Henrik Eichenhardt关于

这是一个关于为什么共享可变性不好的简短推理;摘自博客

非确定性=并行处理+可变状态

这个方程基本上意味着并行处理和 可变状态组合导致程序行为不确定性。 如果您只是执行并行处理,并且只有不可变状态 一切都很好,很容易对程序进行推理。上 另一方面,如果您想对可变数据进行并行处理,您可以 需要同步对可变变量的访问 本质上是将程序的这些部分呈现为单线程。这并不是什么新鲜事,但我还没见过这个概念表达得如此优雅。一个不确定的程序被破坏了

本博客继续推导没有正确同步的并行程序为什么会中断的内部细节,您可以在附加的链接中找到

对第二个示例代码段的解释
列出两个事件2=
numbers.stream()
.filter(e->e%2==0)
.地图(e->e*2)
.collect(toList());//没有副作用!
这将使用
收集器对该流的元素使用collect reduction操作


这更安全、更高效、更易于并行化。

假设两个线程同时执行此任务,第二个线程在第一个线程之后执行一条指令

第一个线程创建DoubleOfEvent。第二个线程创建DoubleOfEvent,第一个线程创建的实例将被垃圾收集。然后两个线程将所有偶数的双精度数添加到doubleOfEvent,因此它将包含0、0、4、4、8、8、12、12。。。而不是0,4,8,12。。。(实际上,这些线程不会完全同步,因此任何可能出错的线程都会出错)


并不是说第二种解决方案更好。您将有两个线程设置相同的全局。在本例中,他们将其设置为逻辑上相等的值,但如果他们将其设置为两个不同的值,则您不知道之后会有哪个值。一个线程不会得到它想要的结果

问题是,讲座同时有点错误。他提供的示例使用了forEach
,记录如下:

此操作的行为显然是不确定的。对于并行流管道,此操作不能保证尊重流的相遇顺序,因为这样做会牺牲并行性的好处

您可以使用:

 numbers.stream()
            .filter(e -> e % 2 == 0)
            .map(e -> e * 2)
            .parallel()
            .forEachOrdered(e -> doubleOfEven.add(e));
你会得到同样的保证结果

另一方面,使用
collector.toList
的示例更好,因为收集器尊重
遭遇顺序,所以它工作得很好

有趣的是,
Collectors.toList
在下面使用了
ArrayList
,它不是线程安全的集合。只是它使用了很多(用于并行处理)并在最后合并

最后一点需要注意的是,并行和顺序不会影响遭遇顺序,而是应用于
的操作会影响遭遇顺序。读得好

我们还需要考虑到,即使使用线程安全的集合也不能完全安全地使用流,特别是当您依赖于
副作用时

 List<Integer> numbers = Arrays.asList(1, 3, 3, 5);
    Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
    List<Integer> collected = numbers.stream()
            .parallel()
            .map(e -> {
                if (seen.add(e)) {
                    return 0;
                } else {
                    return e;
                }
            })
            .collect(Collectors.toList());

    System.out.println(collected);
List number=Arrays.asList(1,3,3,5);
Set seen=Collections.synchronizedSet(新HashSet());
收集的列表=number.stream()
.parallel()
.map(e->{
如果(见添加(e)){
返回0;
}否则{
返回e;
}
})
.collect(Collectors.toList());
系统输出打印项次(已收集);

此时收集的
可能是
[0,3,0,0]
[0,0,3,0]
或其他内容。

在第一个示例中,如果使用parallel(),则无法保证插入(例如,多个线程插入同一元素)


收集(…)另一方面,当并行运行时,在中间步骤中分割工作并在内部收集结果,然后将其添加到最终列表中,以确保顺序和安全。

使流并行,突然,顺序不再得到遵守,或者更糟,列表已损坏,因为多个线程正在并发地对其进行变异,而没有同步。第二个版本不会发生这种情况。@JBNizet在本例中不是因为
并行
(这与结果的顺序无关)
.forEach(e -> doubleOfEven.add(e)); // Unnecessary use of side-effects!
List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList()); // No side-effects! 
 numbers.stream()
            .filter(e -> e % 2 == 0)
            .map(e -> e * 2)
            .parallel()
            .forEachOrdered(e -> doubleOfEven.add(e));
 List<Integer> numbers = Arrays.asList(1, 3, 3, 5);
    Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
    List<Integer> collected = numbers.stream()
            .parallel()
            .map(e -> {
                if (seen.add(e)) {
                    return 0;
                } else {
                    return e;
                }
            })
            .collect(Collectors.toList());

    System.out.println(collected);