Java,哪个线程是顺序流执行的?

时间:2017-08-24 22:03:56

标签: java foreach java-8 java-stream

在阅读有关流的文档时,我遇到了以下句子:

  •   

    ...尝试从行为参数访问可变状态会给你一个错误的选择...如果你没有同步访问那个状态,你就会有数据竞争,因此你的代码被破坏...... [1]

  •   

    如果行为参数确实存在副作用... [没有]保证在同一个线程中执行同一流管道中“相同”元素的不同操作。 [2]

  •   

    对于任何给定元素,可以在任何时间以及库选择的任何线程中执行操作。 [3]

这些句子不区分顺序流和并行流。所以我的问题是:

  1. 在哪个线程中执行顺序流的管道?它总是调用线程还是可以自由选择任何线程?
  2. 如果流是连续的,那么哪个线程是执行的forEach终端操作的action参数?
  3. 使用顺序流时是否必须使用任何同步?

2 个答案:

答案 0 :(得分:1)

  1. Stream的终端操作是阻止操作。如果没有并行执行,执行终端操作的线程将运行管道中的所有操作。
  2.   

    定义1.1。 管道 是一些链式方法。

      

    定义1.2。 中级操作 将位于流中的任何位置,但最后除外。它们返回一个流对象,并且不会在管道中执行任何操作。

      

    定义1.3。 终端操作 将仅位于流的末尾。他们执行管道。它们不返回流对象,因此不能在它们之后添加其他中间操作终端操作

    1. 从第一个解决方案中我们可以得出结论,调用线程将在调用流中的每个元素上的action 终端操作中执行forEach方法。
    2. Java 8向我们介绍了Spliterator接口。它具有Iterator的功能,但也有一组操作可以帮助并行执行和分割任务。

      在顺序执行中从原语流调用forEach时,调用线程将调用Spliterator.forEachRemaining方法:

      @Override
      public void forEach(IntConsumer action) {
         if (!isParallel()) {
              adapt(sourceStageSpliterator()).forEachRemaining(action);
          }
          else {
              super.forEach(action);
          }
      }
      

      您可以在我的教程中Spliterator了解更多内容:Chapter 7: Spliterator

      1. 只要你不改变其中一个流操作中的多个线程之间的任何共享状态(并且它被禁止 - 很快就会解释),当你想要运行时,你不需要使用任何额外的同步工具或算法并行流。
      2. 简化使用accumulatorcombiner函数的流操作,用于执行并行流。根据定义,流库禁止变异。你应该避免它。

        并发和并行编程中有很多定义。我将介绍一组最能为我们服务的定义。

          

        定义8.1。 Cuncurrent编程 是使用其他同步算法解决任务的能力。

          

        定义8.2。 并行编程 是在不使用其他同步算法的情况下解决任务的能力。

        您可以在我的教程中了解更多相关信息:Chapter 8: Parallel Streams

答案 1 :(得分:1)

这一切都归结为基于规范的保证,以及当前实现可能具有超出保证范围的其他行为这一事实。

Java 语言架构师 Brian Goetz 就related question 中的规范提出了相关观点:

<块引用>

存在描述调用者可以依赖的最低保证的规范,而不是描述实现的作用。

[...]

当规范说“不保留属性 X”时,并不意味着属性 X 可能永远不会被观察到;这意味着实施没有义务保留它。 [...](HashSet 不保证迭代其元素会保留它们插入的顺序,但这并不意味着这不会意外发生——你只是不能指望它。)

这一切都意味着,即使当前的实现碰巧具有某些行为特征,也不应该依赖它们,也不应该假设它们不会在库的新版本中发生变化。

顺序流管道线程

<块引用>

在哪个线程中执行顺序流的管道?它始终是调用线程还是可以自由选择任何线程的实现?

当前的流实现可能使用也可能不使用调用线程,并且可能使用一个或多个线程。由于 API 未指定这些内容,因此不应依赖此行为。

forEach 执行线程

<块引用>

如果流是顺序的,forEach终端操作的action参数是在哪个线程中执行的?

虽然当前的实现使用现有线程,但这不能依赖,因为文档指出线程的选择取决于实现。事实上,不能保证元素不会由不同的线程为不同的元素处理,尽管当前的流实现也不会这样做。

根据 API:

<块引用>

对于任何给定的元素,可以在任何时间和在库选择的任何线程中执行操作

请注意,虽然 API 在讨论遭遇顺序时专门调用了并行流,但 Brian Goetz 澄清了这一点,以阐明行为的动机,而不是任何行为都特定于并行流:

<块引用>

在这里明确提出平行案例的目的是教学[...]。但是,对于不了解并行性的读者来说,几乎不可能不假设 forEach 会保留遭遇顺序,因此添加这句话是为了帮助阐明动机。

使用顺序流进行同步

<块引用>

使用顺序流时是否必须使用任何同步?

当前的实现可能会起作用,因为它们对顺序流的 forEach 方法使用单个线程。但是,由于流规范不保证它,因此不应依赖它。因此,应该像方法可以被多个线程调用一样使用同步。

也就是说,stream documentation 特别建议不要使用需要同步的副作用,并建议使用归约操作而不是可变累加器:

<块引用>

许多可能会使用副作用的计算可以更安全、更有效地表达而没有副作用,例如使用归约代替可变累加器。 [...] 少数流操作,例如 forEach() 和 peek(),只能通过副作用进行操作;这些应谨慎使用。

作为如何将不恰当地使用副作用的流管道转换为不使用副作用的示例,以下代码在字符串流中搜索与给定正则表达式匹配的字符串,并将匹配项放入列表中。< /p>

     ArrayList<String> results = new ArrayList<>();
     stream.filter(s -> pattern.matcher(s).matches())
           .forEach(s -> results.add(s));  // Unnecessary use of side-effects!

这段代码不必要地使用了副作用。如果并行执行,ArrayList 的非线程安全性会导致不正确的结果,并且添加所需的同步会导致争用,破坏并行性的好处。此外,在这里使用副作用是完全没有必要的; forEach() 可以简单地替换为更安全、更高效且更适合并行化的归约操作:

     List<String>results =
         stream.filter(s -> pattern.matcher(s).matches())
               .collect(Collectors.toList());  // No side-effects!