Java高性能读取模式
Java应用程序读取数据的方式对其读取性能可能会产生很大的影响。在本文中,我将描述一些不同的读取模式并解释其性能特征。
Read Into New模式
第一个Java读取模式是read-into-new模式。这是我们通常在大学学习的"正确"读取数据的方式。
读入新模式是具有读取方法的模式,该方法可读取某种数据并使用读取的数据返回新的数据结构。首先是一个简单的示例数据结构:
public class MyData { public int val1 = 0; public int val2 = 0; }
这是一个示例读取方法,该方法将数据读取到" MyData"对象中:
public MyData readMyData(byte[] source) { MyData myData = new MyData(); myData.val1 = source[0]; myData.val2 = source[1]; return myData; }
如我们所见,readMyData()方法返回一个MyData对象。首先创建一个" MyData"对象。其次,readMyData()方法将数据读入MyData对象。第三,将MyData对象返回到调用代码。
值得注意的是,每次调用readMyData()方法时,都会返回一个新的MyData对象。这就是为什么将该模式称为"读入新的"的意思,这意味着将数据读入一个新的对象中。
如果频繁调用readMyData()
方法,则会导致创建许多MyData
对象。这给对象分配系统和垃圾收集器带来了压力。这会导致性能降低,并且可能会不时暂停更长的垃圾回收。
读入新模式的另一个缺点是每个对象可能位于计算机内存的不同区域。这意味着该对象成为CPU缓存的机会很小。
Read Into Existing模式
read-into-existing模式会将数据读取到现有对象中,而不是为每个对read方法的调用创建一个新对象。这意味着可以重置同一对象,并将其重用于多次调用read方法。这是使用现有读取模式的早期readMyData()
方法的外观:
public MyData readMyData(byte[] source, MyData myData) { myData.val1 = source[0]; myData.val2 = source[1]; return myData; }
与先前版本到此版本的主要区别在于,该版本采用" MyData"对象将数据读取为参数。现在,由readMyData()方法的调用者决定是否应该重用现有的MyData实例,或者是否应该创建一个新实例。
与始终创建新实例相比,重用MyData实例而不是创建新实例将节省时间和内存。它还将减轻Java垃圾收集器的压力,因此减少了长时间垃圾收集暂停的风险。
重用一个对象还意味着该对象位于CPU缓存中的机会比为每次调用readMyData()方法创建一个新对象时要高得多。
读出
读出模式read-out-of不会将数据读入对象。相反,它直接从基础数据源读取所需的值。
直接从数据源读取值可以节省一些时间,因为在使用数据之前不需要先将数据复制到对象中。需要时,将值直接从基础数据源中复制出来。
直接从数据源读取值还具有以下优点:仅将实际使用的数据从底层数据源中复制出来。因此,如果读取代码仅需要部分数据,则仅复制那些部分。
要更改前面的示例代码以直接从基础源读取数据,我们需要更改MyData类的实现:
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]; } }
要在新版本中使用MyData类,我们将使用如下代码:
byte[] source = ... //get bytes from somewhere MyData myData = new MyData(); myData.setSource(source); int val1 = myData.getVal1(); int val2 = myData.getVal2();
首先请注意,我们可以重用MyData实例。当我们需要从新的字节数组中读取数据时,只需调用setSource()即可。
其次,仅使用该值将数据从字节数组复制到代码一次。它不会首先从字节数组复制到" MyData"对象,然后再从那里复制到需要该值的任何计算。
第三,只有当我们同时调用getVal1()和getVal2()时,才会从底层字节数组中读取相应的数据。如果计算仅需要一个值,则仅需要从字节数组中读取该值。仅使用部分数据时,这样可以节省时间。
将数据读入对象的read方法通常不知道需要多少数据。因此,将所有数据复制到对象中是正常的。除非我们创建针对每种计算量身定制的多种读取方法,否则将为印版增加更多工作。
导航
如果基础数据源包含多个"记录"或者"对象",则可以将读出模式更改为导航器模式。导航器模式的工作方式类似于读出模式,但是增加了在底层源中的记录或者对象之间进行导航的方法。
假设每个" MyData"对象都包含来自底层源的2个字节,这是添加了导航方法的" 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; } }
第一个变化是setSource()方法现在采用了一个名为offset的额外参数。这不是严格必要的,但是这使MyData导航器可以从偏移量开始到源字节数组,而不是第一个字节。
第二个变化是,当读取值时,getVal1()和getVal2()方法现在使用内部offset
变量的值作为源数组的索引。
第三个变化是添加了next()
方法。 " next()"方法将内部" offset"变量增加2,从而使" offset"变量指向数组中的下一条记录。
第四个变化是添加了" hasNext()"方法,如果源字节数组中包含更多记录(字节),该方法将返回true。
我们可以使用" MyData"的导航器版本,如下所示:
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(); }
如我们所见,在导航器模式实现中使用MyData类非常简单。与使用标准JavaIterator
非常相似。