【问题标题】:Why is shared mutability bad?为什么共享可变性不好?
【发布时间】:2017-10-28 10:18:33
【问题描述】:

我正在观看有关 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());

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

【问题讨论】:

  • 使流并行,突然之间,顺序不再被遵守,或者更糟糕的是,由于多个线程在没有同步的情况下同时改变它,因此列表已损坏。第二个版本不会发生这种情况。
  • @JBNizet 在示例中不是因为parallel(与结果集合的顺序无关),而是关于forEach
  • @Eugene 没有共享,只有一个线程。所以,既然这是关于共享可变性,是的,这是关于并行的。是的,即使使用同步集合,使该代码并行,也会使集合的顺序不确定(实际上已经不能保证在没有并行的情况下具有确定的顺序,即使在实践中也是如此)。
  • @JBNizet 应该重新表述一下。由于应用了parallel,所以没有顺序因为 forEach。我的观点是并行/顺序并不决定顺序。是操作。
  • @Eugene 好的,我同意。

标签: java java-8 java-stream immutability


【解决方案1】:

第一个例子sn-p的解释

在执行并行处理时会出现问题。

//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 的博客,回复了 why a shared mutable state is the root of all evil.

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

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

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

本博客继续推导出没有正确同步的并行程序为何会被破坏的内部细节,您可以在附加的链接中找到这些细节。

第二个例子sn-p的说明

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

这使用Collector 对该流的元素进行收集减少 操作。

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

【讨论】:

  • 即使在一个可变的同步列表的情况下,您最终也会在列表上发生线程争用,从而使并行处理几乎毫无用处。
  • 该摘录过于简单化了情况,但根据经验,这很好。通过对其中一个设置一些限制,您可以拥有并行处理和可变状态,而不会失去确定性。例如,使用晶格变量会部分限制可变性。显然,各种形式的同步或协调限制了并行性而不限制可变性。子程序可以是非确定性的,而整个程序仍然符合确定性规范,并且非确定性可以是规范的一部分,因此程序不会因非确定性而固有地被破坏。
  • @FedericoPeraltaSchaffner 我认为这句话的重点是“如果你想对可变数据进行并行处理,你需要同步对可变变量的访问,这基本上呈现程序的这些部分单线程。”
【解决方案2】:

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

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

你可以使用:

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

而且您将始终获得相同的保证结果。

另一方面,使用Collectors.toList 的例子更好,因为收集器尊重encounter order,所以它工作得很好。

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

最后一点,并行和顺序不会影响相遇顺序,它是应用于Stream 的操作。精彩阅读here

我们还需要考虑,即使使用线程安全的集合仍然不能完全安全地使用 Streams,尤其是当您依赖 side-effects 时。

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

此时collected 可能是[0,3,0,0][0,0,3,0] 或其他。

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多