Board logo

标题: java.util.stream 库简介(3) [打印本页]

作者: look_w    时间: 2018-6-24 14:38     标题: java.util.stream 库简介(3)

并行性将计算构建为功能转换的一个有益的结果是,您只需对代码进行极少的更改,即可轻松地在顺序和并行执行之间切换。流计算的顺序表达和相同计算的并行表达几乎相同。清单                4 展示了如何并行地执行  中的查询。
清单 4.                    清单 1                的并行版本
1
2
3
4
5
int totalSalesFromNY
    = txns.parallelStream()
          .filter(t -> t.getSeller().getAddr().getState().equals("NY"))
          .mapToInt(t -> t.getAmount())
          .sum();




“将流管道表达为一系列功能转换,有助于实施一些有用的执行战略,比如惰性、并行性、短路和操作融合。”

第一行将会请求一个并行流而不是顺序流,这是与  的唯一区别,因为 Streams                库有效地从执行计算的战略中分解出了计算的描述和结构。以前,并行执行要求完全重写代码,这样做不仅代价高昂,而且往往容易出错,因为得到的并行代码与顺序版本不太相似。
所有流操作都可以顺序或并行执行,但请记住,并行性并不是高性能的原因。并行执行可能比顺序执行更快、一样快或更慢。最好首先从顺序流开始,在您知道您能够获得提速(并从中受益)时才应用并行性。本系列后面的一期文章会返回分析流管道的并行性能。
附加信息尽管 Streams                库是为计算而精心设计的,但执行计算涉及到回调客户端所提供的拉姆达表达式,这些拉姆达表达式的用途具有一定的限制。违反这些限制可能导致流管道失败或计算出不正确的结果。此外,对于具有副作用的拉姆达表达式,这些副作用的时限(或存在)可能在某些情况下不合情理。
大多数流操作都要求传递给它们的拉姆达表达是互不干扰无状态                的。互不干扰意味着它们不会修改流来源;无状态意味着它们不会访问(读或写)任何可能在流操作寿命内改变的状态。对于缩减操作(例如计算                sum、min 或 max                    等汇总数据),传递给这些操作的拉姆达表达式必须是结合性 的(或遵守类似的要求)。
从某种程度讲,这些要求源于以下事实:如果管道并行执行,流库可能从多个线程访问数据源,或并发地调用这些拉姆达表达式。需要这些限制才能确保计算保持正确。(这些限制也可能得到更加简单、更容易理解的代码,无论是否采用并行性。)您可能倾向于让自己相信,您可以忽略这些限制,因为您认为特定的管道从不会并行运行,但最好控制住这一倾向,否则您会在代码中埋下定时炸弹。花点精力来表达您的流管道,使得无论采用何种执行战略,它们都是正确的。
所有并发性风险的根源是共享可变状态。共享可变状态的一种可能来源是流来源。如果来源是像 ArrayList                这样的传统集合,Streams 库会假设它在流操作过程中保持不变。(明显为了实现并发访问而设计的集合,比如                ConcurrentHashMap,不符合这一假设。)互不干扰要求不仅不包括在流操作期间被其他操作突变的来源,而且传递给流操作的拉姆达表达式本身也应避免突变来源。
除了不修改流来源之外,传递给流操作的拉姆达表达式也应是无状态的。例如,清单 5                中的代码(尝试消除任何与前面的元素重复的元素)就违背了这一规则。
清单 5.                使用有状态拉姆达表达式的流管道(不要这么做!)
1
2
3
4
5
6
7
8
HashSet<Integer> twiceSeen = new HashSet<>();
int[] result
    = elements.stream()
              .filter(e -> {
                  twiceSeen.add(e * 2);
                  return twiceSeen.contains(e);
              })
              .toArray();




如果并行执行,此管道会生成错误的结果,原因有两个。首先,对 twiceSeen                集的访问是从多个线程进行的,没有进行任何协调,因此不是线程安全的。第二,因为数据被分区了,所以无法确保在处理给定元素时已经处理了该元素前面的所有元素。
最好的情况是,如果传递给流操作的拉姆达表达式完全没有副作用,也就是说,它们不会突变任何基于堆的状态或在执行过程中执行任何                I/O。如果有副作用,它们应负责执行任何需要的协调,以确保这些副作用是线程安全的。
此外,无法保证所有副作用都将执行。例如,在清单 6 中,该库被释放了,以完全避免执行传递给 map()                的拉姆达表达式。因为来源具有已知大小,map()                操作被认为会保持该大小,而且映射不会影响计算的结果,所以库可以通过完全不执行映射来优化计算!(这种优化可以将计算从 O(n)                转换到 O(1),还可以消除与调用映射函数相关的工作。)
清单 6.                    具有可能不会被执行的副作用的流管道
1
2
3
4
int count =
    anArrayList.stream()
               .map(e -> { System.out.println("Saw " + e); e })
               .count();




您会注意到受这种优化影响的唯一情况(除了计算速度快得多)是,传递给 map() 的拉姆达表达式具有副作用 —                在这种情况下,如果这些副作用没有发生,您可能感到非常奇怪。能够实现这些优化的假设前提是,流操作属于功能转换。在大多数时候,该库使我们的代码能够运行得更快,而且不需要我们投入精力。能够执行这样的优化的代价是,我们必须接受对我们传递给流操作的拉姆达表达式的操作的一些限制,以及我们对副作用的一定的依赖。(总之,这是一次很划算的交易。)




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0