Java 为什么共享可变性不好?
我在看一个关于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)); 然
//将偶数值加倍并将其放入列表中。
列表编号=数组.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);