java 高性能读模式(译)
原文地址:http://tutorials.jenkov.com/java-performance/read-patterns.html
好久没翻译了,逛知乎,无意间发现的一个链接,写的太好了,而且内容也不多(博主有点懒了~),忍不住翻译下,希望园子的朋友可以一起学习
--------------------------------------------------------------我是华丽的分割线--------------------------------------------------------------
如何读取数据将很大程度影响你的java应用。在本篇文章中,我将介绍几种读取模式,并解释他们的性能特性
写到新对象中
第一种读模式叫做写到新的对象中。这种就是我们在大学学习中,被冠以"正确"的读取数据的一种典型方式。
读取新对象模式,有一个返回一个全新对象结构的方法,下面例子来说明:
public class MyData { public int val1 = 0; public int val2 = 0; }
读取方法
public MyData readMyData(byte[] source) { MyData myData = new MyData(); myData.val1 = source[0]; myData.val2 = source[1]; return myData; }
正如我们所见,方法会返回一个MyData对象。首先,MyData对象会被先创建。其次,方法将数据写到创建好的MyData对象中,最后,方法返回创建好的这个对象
值得注意的是,每次调用readMyData()方法,都将创建一个全新的MyData对象。这也是为什么这种模式叫做写到新对象。
加入readMyData()方法被频繁的调用,那么他讲创建无数的MyData对象。这会被系统和垃圾回收器带来压力。结果也就导致性能低下,甚至可能时候造成垃圾回收中断。
另外一个写到新对象模式的劣势就是每个对象可能被存储在不同的内存区域。这也意味着CPU缓存的效率会很低。
读取到已存在对象
读取到已存在对象模式是从已有对象中读取对象来代替创建一个全新的对象。这也意味着相同的对象可以被重置和重复利用多次。让我们将上面的方法改成读取到已存在对象模式:
public MyData readMyData(byte[] source, MyData myData) { myData.val1 = source[0]; myData.val2 = source[1]; return myData; }
两种模式最主要的区别在于,方法参数中增加了MyData的对象参数。这样,我们可以让调用方法着决定是使用已有的对象还是重新创建一个新的对象。
相比于总是创建一个新的MyData,次模式可以减少MyData实例来节省时间和内存。同时对于垃圾回收造成的压力也更小。长时间垃圾回收中断的风险也会降低。
减少对象同时意味着滞留在CPU缓存中的机会比每次创建一个对象的模式来的更高。
从外部读取
从外部读取模式并不会将读取到的数据写到对象中。而是直接从底层数据中读取所需要的数据
直接从数据源读取数据可以节省一些时间,因为在它们(方法或则变量)在使用前,不需要赋值。当需要的时候,直接从底层数据源中拷贝值过来即可
这种方式还有一个优势在于实际需要多少数据,就拷贝多少,而不需要全部赋值
将上述的例子改造成这种模式,如下:
public class MyData() { private byte[] source = null; public MyData() { } public void setSource(byte[] source) { this.source = source; } public int getVal1() { return this.source[0]; } public int getVal2() { return this.source[1]; } }
然后,我们这样使用它
byte[] source = ... //get bytes from somewhere MyData myData = new MyData(); myData.setSource(source); int val1 = myData.getVal1(); int val2 = myData.getVal2();
首先注意到,我们仅仅通过改变source数组,就可以复用MyData实例。
其次,数据从数组字节中拷贝仅需要一次。
第三,当我们只需要部分值计算的时候,我们只需要调用其中任意一个方法,这样就节省了时间。除非是我们需要同时调用这两个方法
一个只读方法,并不知道有多少个变量值是需要被读取的。因此,通常的做法是将所有的值都拷贝到对象中。除非你定制多个计算方法,不过这样有增加了额外的工作量
导航
如果底层数据源包含了多个"记录"或则"对象",我们可以将从外部读取模式变成导航模式。导航模式的工作原理跟从外部读取类似,但是在方法上新增了从底层数据源中读取数据的导航
假设每个MyData对象由底层数据源中的两个字节组成,让我们先看下下面的例子:
public class MyData() { private byte[] source = null; private int offset = 0; public MyData() { } public void setSource(byte[] source, int offset) { this.source = source; this.offset = offset; } public int getVal1() { return this.source[this.offset]; } public int getVal2() { return this.source[this.offset + 1]; } public void next() { this.offset += 2; //2 bytes per record } public boolean hasNext() { this.offset < this.source.length; } }
首先改变的是setResource()方法新增了一个额外的参数offset。严格来说,offset是非必须的,但是它(offset)可以让我们读取数据的索引从默认的第一位改成offset定义的
第二个变化是getVal1()和getVal2()方法现在使用内部的offset变化作为读取数组的索引
第三个变化是新增了next()方法。next()方法会将内部的offset值加2,这样offset将指向下一条数组记录
第四个变化新增了hasNext()方法,该方法将返回数据源是否还有可供查询的数据记录。
你可以使用如下导航方法
byte[] source = ... // get byte array from somewhere MyData myData = new MyData(); myData.setSource(source, 0); while(myData.hasNext()) { int val1 = myData.getVal1(); int val2 = myData.getVal2(); myData.next(); }
正如你所看到的,使用导航模式是相关直观的。非常镜像于java标准的迭代器