当前位置: 首页 > 知识库问答 >
问题:

为什么共享可变性不好?

曾歌者
2023-03-14

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

“可变是可以的,共享是好事,共享可变是魔鬼的工作。”

他所指的就是以下代码,他认为这是一个“极其糟糕的习惯”:

//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(e -> doubleOfEven.add(e));

然后,他继续编写应该使用的代码,即:

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList());

我不明白为什么第一段代码是“坏习惯”。对我来说,他们都达到了相同的目标。

共有3个答案

燕航
2023-03-14

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

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

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

马承
2023-03-14

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

此操作的行为是显式非确定性的。对于并行流管道,此操作不保证遵循流的遭遇顺序,因为这样做会牺牲并行性的好处...

您可以使用:

 numbers.stream()
            .filter(e -> e % 2 == 0)
            .map(e -> e * 2)
            .parallel()
            .forEachOrdered(e -> doubleOfEven.add(e));

你总是会得到同样的保证结果。

另一方面,使用<code>收集器的示例。toList更好,因为收集器尊重遭遇顺序,所以工作正常。

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

最后要注意的是,并行和顺序不会影响遭遇顺序,而是应用于流的操作。在这里阅读得很好。

我们还需要考虑,即使使用线程安全的集合在Streams中仍然不完全安全,尤其是当您依赖副作用时。

 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);

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

杨君之
2023-03-14

这个问题在执行并行处理时起作用。

//double the even values and put that into a list.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 1, 2, 3, 4, 5);
List<Integer> doubleOfEven = new ArrayList<>();

numbers.stream()
       .filter(e -> e % 2 == 0)
       .map(e -> e * 2)
       .forEach(e -> doubleOfEven.add(e)); // <--- Unnecessary use of side-effects!

这不必要地使用了副作用,而如果使用正确,并非所有副作用都是不好的。在使用流时,必须提供可以安全地在输入的不同部分同时执行的行为。即编写不访问共享可变数据的代码来完成工作。

该行:

.forEach(e -> doubleOfEven.add(e)); // Unnecessary use of side-effects!

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

不久前,我读了亨里克·艾肯哈特(Henrik Eichenhardt)的一篇博客,回答了为什么共享的可变状态是所有邪恶的根源。

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

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

这个等式基本上意味着并行处理和可变状态相结合会导致非确定性程序行为。如果你只是做并行处理并且只有不可变状态,一切都很好,并且很容易对程序进行推理。另一方面,如果你想对可变数据进行并行处理,你需要同步对可变变量的访问,这基本上使程序的这些部分成为单线程的。这并不是什么新鲜事,但我从未见过这个概念表达得如此优雅。非确定性程序坏了。

此博客继续推导内部详细信息,说明为什么没有正确同步的并行程序会被破坏,您可以在附加的链接中找到这些详细信息。

List<Integer> doubleOfEven2 =
      numbers.stream()
             .filter(e -> e % 2 == 0)
             .map(e -> e * 2)
             .collect(toList()); // No side-effects! 

这将使用收集器对该流的元素进行收集缩减操作。

这更安全、更高效,也更适合并行化。

 类似资料:
  • 问题内容: 我正在观看有关Java的演示,有一次讲师说: “可变性还可以,共享很好,共享可变性是魔鬼的工作。” 他指的是以下代码,他认为这是“极度坏习惯”: 然后,他着手编写应使用的代码,即: 我不明白为什么第一段代码是“不良习惯”。对我来说,他们都实现了相同的目标。 问题答案: 当执行并行处理时,该问题就起作用了。 在使用流时,这不必要地使用了 副作用, 但如果正确使用,并非所有副作用都不好,因

  • 正如你可以看到在这种情况下,虽然我们更改了的值,的值没有改变。 这是因为的类型,是不可变的。 在JavaScript内置类型中,以下是不可变的: Boolean Number Symbol Null Object Array String是一种不常见的情况,因为它可以使用进行迭代,并像数组一样提供数字索引器,但会执行以下操作:

  • 本文向大家介绍String 为什 么是不可变的?相关面试题,主要包含被问及String 为什 么是不可变的?时的应答技巧和注意事项,需要的朋友参考一下 简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 Ab

  • 场景:nestjs modules 使用app.service 是否需要注册? 我想实现的大致如下: 以上不行!? 理论上来说我在app.module.ts中使用了providers应该是全局共享的呀?还是说我理解的有错误? 预期:在其他的modules使用app的方法的时候可以不用注册、辛苦有相关的伙伴指点一下、感谢

  • 问题内容: 每个线程都有自己的堆栈,但是它们共享一个公共堆。 所有人都清楚堆栈是用于局部/方法变量,堆是用于实例/类变量。 在线程之间共享堆有什么好处。 有多个线程同时运行,因此共享内存可能导致诸如并发修改,互斥等开销的问题。堆中的线程共享哪些内容。 为什么会这样呢?为什么每个线程也不拥有自己的堆?谁能提供一个现实的例子,线程如何利用共享内存? 问题答案: 要将数据从一个线程传递到另一个线程时该怎

  • 本文向大家介绍什么是秘密共享?相关面试题,主要包含被问及什么是秘密共享?时的应答技巧和注意事项,需要的朋友参考一下 回答:秘密共享是用于在区块链中提供数据安全性的主要方法之一。这种方法将个人信息或机密信息分为不同的单元,然后将其发送给网络上的用户。原始信息共享给分配了秘密共享的参与者。