Java8中流的性能及流的几个特性
摘要:本文介绍了java8中流的几个特性,以告诫开发者流并不是高性能的代名词,需谨慎使用流。以下是译文。
流(stream)是java8为了实现最佳性能而引入的一个全新的概念。在过去的几年中,随着硬件的持续发展,编程方式已经发生了巨大的改变,程序的性能也随着并行处理、实时、云和其他一些编程方法的出现而得到了不断提高。
java8中,流性能的提升是通过并行化(parallelism)、惰性(laziness)和短路操作(short-circuit operations)来实现的。但它也有一个缺点,在选择流的时候需要非常小心,因为这可能会降低应用程序的性能。
下面来看看这三项支撑起流强大性能的因素吧。
并行化
流的并行化充分利用了硬件的相关功能。由于现在计算机上通常都有多个cpu核心,所以在多核系统中如果只使用一个线程则会极大地浪费系统资源。设计和编写多线程应用非常具有挑战性,并且很容易出错,因此,流存在两种实现:顺序和并行。使用并行流非常简单,无需专业知识即可轻松处理多线程问题。
在java的流中,并行化是通过fork-join原理来实现的。根据fork-join原理,系统会将较大的任务切分成较小的子任务(称之为forking),然后并行处理这些子任务以充分利用所有可用的硬件资源,最后将结果合并起来(称之为join)组成完整的结果。
在选择顺序和并行的时候,需要非常谨慎,因为并行并一定意味着性能会更好。
让我们来看一个例子。
streamtest.java:
package test; import java.util.arraylist; import java.util.list; public class streamtest { static list < integer > mylist = new arraylist < > (); public static void main(string[] args) { for (int i = 0; i < 5000000; i++) mylist.add(i); int result = 0; long loopstarttime = system.currenttimemillis(); for (int i: mylist) { if (i % 2 == 0) result += i; } long loopendtime = system.currenttimemillis(); system.out.println(result); system.out.println("loop total time = " + (loopendtime - loopstarttime)); long streamstarttime = system.currenttimemillis(); system.out.println(mylist.stream().filter(value -> value % 2 == 0).maptoint(integer::intvalue).sum()); long streamendtime = system.currenttimemillis(); system.out.println("stream total time = " + (streamendtime - streamstarttime)); long parallelstreamstarttime = system.currenttimemillis(); system.out.println(mylist.parallelstream().filter(value -> value % 2 == 0).maptoint(integer::intvalue).sum()); long parallelstreamendtime = system.currenttimemillis(); system.out.println("parallel stream total time = " + (parallelstreamendtime - parallelstreamstarttime)); } }
运行结果:
820084320 loop total time = 17 820084320 stream total time = 81 820084320 parallel stream total time = 30
正如你所见,在这种情况下,for循环更好。因此,在没有正确的分析之前,不要用流代替for循环。在这里,我们可以看到,并行流的性能比普通流更好。
注意:结果可能会因为硬件的不同而不同。
惰性
我们知道,java8的流有两种类型的操作,分别为中间操作(intermediate)和最终操作(terminal)。这两种操作分别用于处理和提供最终结果。如果最终操作不与中间操作相关联,则无法执行。
总之,中间操作只是创建另一个流,不会执行任何处理,直到最终操作被调用。一旦最终操作被调用,则开始遍历所有的流,并且相关的函数会逐一应用到流上。中间操作是惰性操作,所以,流支持惰性。
注意:对于并行流,并不会在最后逐个遍历流,而是并行处理流,并且并行度取决于机器cpu核心的个数。
考虑一下这种情况,假设我们有一个只有中间操作的流片段,而最终操作要稍后才会添加到应用中(可能需要也可能不需要,取决于用户的需求)。在这种情况下,流的中间操作将会为最终操作创建另一个流,但不会执行实际的处理。这种机制有助于提高性能。
我们来看一下有关惰性的例子:
streamlazinesstest.java:
package test; import java.util.arraylist; import java.util.list; import java.util.stream.collectors; import java.util.stream.stream; public class streamlazinesstest { /** employee class **/ static class employee { int id; string name; public employee(int id, string name) { this.id = id; this.name = name; } public string getname() { return this.name; } } public static void main(string[] args) throws interruptedexception { list < employee > employees = new arraylist < > (); /** creating the employee list **/ for (int i = 1; i < 10000000; i++) { employees.add(new streamlazinesstest.employee(i, "name_" + i)); } /** only intermediate operations; it will just create another streams and * will not perform any operations **/ stream < string > employeenamestreams = employees.parallelstream().filter(employee -> employee.id % 2 == 0) .map(employee -> { system.out.println("in map - " + employee.getname()); return employee.getname(); }); /** adding some delay to make sure nothing has happen till now **/ thread.sleep(2000); system.out.println("2 sec"); /** terminal operation on the stream and it will invoke the intermediate operations * filter and map **/ employeenamestreams.collect(collectors.tolist()); } }
运行上面的代码,你可以看到在调用最前操作之前,中间操作不会被执行。
短路行为
这是优化流处理的另一种方法。 一旦条件满足,短路操作将会终止处理过程。 有许多短路操作可供使用。 例如,anymatch、allmatch、findfirst、findany、limit等等。
我们来看一个例子。
streamshortcircuittest.java: package test; import java.util.arraylist; import java.util.list; import java.util.stream.collectors; import java.util.stream.stream; public class streamshortcircuittest { /** employee class **/ static class employee { int id; string name; public employee(int id, string name) { this.id = id; this.name = name; } public int getid() { return this.id; } public string getname() { return this.name; } } public static void main(string[] args) throws interruptedexception { list < employee > employees = new arraylist < > (); for (int i = 1; i < 10000000; i++) { employees.add(new streamshortcircuittest.employee(i, "name_" + i)); } /** only intermediate operations; it will just create another streams and * will not perform any operations **/ stream < string > employeenamestreams = employees.stream().filter(e -> e.getid() % 2 == 0) .map(employee -> { system.out.println("in map - " + employee.getname()); return employee.getname(); }); long streamstarttime = system.currenttimemillis(); /** terminal operation with short-circuit operation: limit **/ employeenamestreams.limit(100).collect(collectors.tolist()); system.out.println(system.currenttimemillis() - streamstarttime); } }
运行上面的代码,你会看到性能得到了极大地提升,在我的机器上只需要6毫秒的时间。 在这里,limit()方法在满足条件的时候会中断运行。
最后要注意的是,根据状态的不同有两种类型的中间操作:有状态(stateful)和无状态(stateless)中间操作。
有状态中间操作
这些中间操作需要存储状态,因此可能会导致应用程序的性能下降,例如,distinct()、sort()、limit()等等。
无状态中间操作
这些中间操作可以独立处理,因为它们不需要保存状态,例如, filter(),map()等。
在这里,我们了解到,流的出现是为了获得更高的性能,但并不是说使用了流之后性能肯定会得到提升,因此,我们需要谨慎使用。
总结
以上所述是小编给大家介绍的java8中流的性能及流的几个特性,希望对大家有所帮助
推荐阅读