在使用Java编程语言时,我们将继续讨论与建议的实践有关的系列文章,我们将讨论并演示如何将对象序列化用于高性能应用程序。
所有讨论的主题均基于用例,这些用例来自于电信行业的关键任务超高性能生产系统的开发。
在阅读本文的每个部分之前,强烈建议您参考相关的Java API文档以获取详细信息和代码示例。
所有测试均针对具有以下特征的Sony Vaio进行:
- 系统:openSUSE 11.1(x86_64)
- 处理器(CPU):Intel(R)Core(TM)2 Duo CPU T6670 @ 2.20GHz
- 处理器速度:1,200.00 MHz
- 总内存(RAM):2.8 GB
- Java:OpenJDK 1.6.0_0 64位
应用以下测试配置:
- 并发工作者线程:200
- 每个工作人员重复测试的次数:1000
- 整体测试次数:100
高性能序列化
序列化是将对象转换为字节流的过程。 然后可以通过套接字发送该流,将其存储到文件和/或数据库中,或者直接按原样对其进行操作。 在本文中,我们不打算对序列化机制进行深入的描述,有许多文章提供了这种信息。 这里将讨论的是我们利用序列化以实现高性能结果的主张。
序列化的三个主要性能问题是:
- 序列化是一种递归算法。 从单个对象开始,通过实例变量可以从该对象访问的所有对象也被序列化。 默认行为很容易导致不必要的序列化开销
- 序列化和反序列化都需要序列化机制来发现有关其序列化实例的信息。 使用默认的序列化机制,将使用反射来发现所有字段值。 此外,如果您未明确设置“ serialVersionUID”类属性,则序列化机制必须对其进行计算。 这涉及遍历所有字段和方法以生成哈希。 上述过程可能很慢
- 使用默认的序列化机制,所有序列化类描述信息都包含在流中,例如:
- 所有可序列化超类的描述
- 类本身的描述
- 与类的特定实例相关联的实例数据
要解决上述性能问题,可以改用外部化。 这两种方法之间的主要区别在于,序列化将所有可序列化超类的类描述以及与该实例相关联的信息(当被视为每个单独的超类的实例)写出。 另一方面,外部化将写出类的标识(类的名称和适当的“ serialVersionUID”类属性)以及超类结构以及有关类层次结构的所有信息。 换句话说,它存储所有元数据,但仅写出本地实例信息。 简而言之,外部化几乎消除了序列化机制使用的所有反射调用,使您可以完全控制编组和解组算法,从而显着提高性能。
当然,外部化效率是有代价的。 由于从类定义中自动提取了元数据,因此默认的序列化机制可适应应用程序更改。 另一方面,外部化不是很灵活,需要您在更改类定义时重写编组和解组代码。
以下是有关如何将外部化用于高性能应用程序的简短演示。 我们将从提供“ Employee”对象开始执行序列化和反序列化操作。 将使用两种类型的“ Employee”对象。 一种适合标准序列化操作,另一种经过修改以便可以外部化。
以下是“雇员”对象的第一种味道:
package com.javacodegeeks.test;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class Employee implements Serializable {
private static final long serialVersionUID = 3657773293974543890L;
private String firstName;
private String lastName;
private String socialSecurityNumber;
private String department;
private String position;
private Date hireDate;
private Double salary;
private Employee supervisor;
private List<string> phoneNumbers;
public Employee() {
}
public Employee(String firstName, String lastName,
String socialSecurityNumber, String department, String position,
Date hireDate, Double salary) {
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityNumber = socialSecurityNumber;
this.department = department;
this.position = position;
this.hireDate = hireDate;
this.salary = salary;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getSocialSecurityNumber() {
return socialSecurityNumber;
}
public void setSocialSecurityNumber(String socialSecurityNumber) {
this.socialSecurityNumber = socialSecurityNumber;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public Date getHireDate() {
return hireDate;
}
public void setHireDate(Date hireDate) {
this.hireDate = hireDate;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Employee getSupervisor() {
return supervisor;
}
public void setSupervisor(Employee supervisor) {
this.supervisor = supervisor;
}
public List<string> getPhoneNumbers() {
return phoneNumbers;
}
public void setPhoneNumbers(List<string> phoneNumbers) {
this.phoneNumbers = phoneNumbers;
}
}
这里要注意的事情:
- 我们假设以下字段是必填字段:
- “名字”
- “姓”
- “社会安全号码”
- “部”
- “位置”
- “雇用日期”
- “薪水”
以下是“雇员”对象的第二种风味:
package com.javacodegeeks.test;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class Employee implements Externalizable {
private String firstName;
private String lastName;
private String socialSecurityNumber;
private String department;
private String position;
private Date hireDate;
private Double salary;
private Employee supervisor;
private List<string> phoneNumbers;
public Employee() {
}
public Employee(String firstName, String lastName,
String socialSecurityNumber, String department, String position,
Date hireDate, Double salary) {
this.firstName = firstName;
this.lastName = lastName;
this.socialSecurityNumber = socialSecurityNumber;
this.department = department;
this.position = position;
this.hireDate = hireDate;
this.salary = salary;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getSocialSecurityNumber() {
return socialSecurityNumber;
}
public void setSocialSecurityNumber(String socialSecurityNumber) {
this.socialSecurityNumber = socialSecurityNumber;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
public Date getHireDate() {
return hireDate;
}
public void setHireDate(Date hireDate) {
this.hireDate = hireDate;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public Employee getSupervisor() {
return supervisor;
}
public void setSupervisor(Employee supervisor) {
this.supervisor = supervisor;
}
public List<string> getPhoneNumbers() {
return phoneNumbers;
}
public void setPhoneNumbers(List<string> phoneNumbers) {
this.phoneNumbers = phoneNumbers;
}
public void readExternal(ObjectInput objectInput) throws IOException,
ClassNotFoundException {
this.firstName = objectInput.readUTF();
this.lastName = objectInput.readUTF();
this.socialSecurityNumber = objectInput.readUTF();
this.department = objectInput.readUTF();
this.position = objectInput.readUTF();
this.hireDate = new Date(objectInput.readLong());
this.salary = objectInput.readDouble();
int attributeCount = objectInput.read();
byte[] attributes = new byte[attributeCount];
objectInput.readFully(attributes);
for (int i = 0; i < attributeCount; i++) {
byte attribute = attributes[i];
switch (attribute) {
case (byte) 0:
this.supervisor = (Employee) objectInput.readObject();
break;
case (byte) 1:
this.phoneNumbers = Arrays.asList(objectInput.readUTF().split(";"));
break;
}
}
}
public void writeExternal(ObjectOutput objectOutput) throws IOException {
objectOutput.writeUTF(firstName);
objectOutput.writeUTF(lastName);
objectOutput.writeUTF(socialSecurityNumber);
objectOutput.writeUTF(department);
objectOutput.writeUTF(position);
objectOutput.writeLong(hireDate.getTime());
objectOutput.writeDouble(salary);
byte[] attributeFlags = new byte[2];
int attributeCount = 0;
if (supervisor != null) {
attributeFlags[0] = (byte) 1;
attributeCount++;
}
if (phoneNumbers != null && !phoneNumbers.isEmpty()) {
attributeFlags[1] = (byte) 1;
attributeCount++;
}
objectOutput.write(attributeCount);
byte[] attributes = new byte[attributeCount];
int j = attributeCount;
for (int i = 0; i < 2; i++)
if (attributeFlags[i] == (byte) 1) {
j--;
attributes[j] = (byte) i;
}
objectOutput.write(attributes);
for (int i = 0; i < attributeCount; i++) {
byte attribute = attributes[i];
switch (attribute) {
case (byte) 0:
objectOutput.writeObject(supervisor);
break;
case (byte) 1:
StringBuilder rowPhoneNumbers = new StringBuilder();
for(int k = 0; k < phoneNumbers.size(); k++)
rowPhoneNumbers.append(phoneNumbers.get(k) + ";");
rowPhoneNumbers.deleteCharAt(rowPhoneNumbers.lastIndexOf(";"));
objectOutput.writeUTF(rowPhoneNumbers.toString());
break;
}
}
}
}
这里要注意的事情:
- 我们实现了“ writeExternal”方法来编组“ Employee”对象。 所有必填字段都写入流
- 对于“ hireDate”字段,我们仅写入此Date对象表示的毫秒数。 假设demarshaller将使用与marshaller相同的时区,毫秒值就是我们正确反序列化“ hireDate”字段所需的所有信息。 请记住,我们可以使用“ objectOutput.writeObject(hireDate)”操作来序列化整个“ hireDate”对象。 在这种情况下,默认的序列化机制将导致结果流的速度下降和大小增加
- 所有非强制性字段(“ supervisor”和“ phoneNumbers”)只有在它们具有实际(非空)值时才被写入流中。 为了实现此功能,我们使用“ attributeFlags”和“ attributes”字节数组。 “ attributeFlags”数组的每个位置代表一个非强制性字段,并保留一个“标记”,指示特定字段是否具有值。 我们检查每个非必填字段,并使用相应的标记填充“ attributeFlags”字节数组。 “属性”字节数组指示必须通过“位置”写入流中的实际非必需字段。 例如,如果“ supervisor”和“ phoneNumbers”非必填字段均具有实际值,则“ attributeFlags”字节数组应为[1,1],而“ attributes”字节数组应为[0,1]。 如果仅“ phoneNumbers”非强制字段具有非空值,则“ attributeFlags”字节数组应为[0,1],而“ attributes”字节数组应为[1]。 通过使用上述算法,我们可以为结果流实现最小的尺寸占用。 为了正确地反序列化“ Employee”对象的非必需参数,我们必须仅将以下信息写入流:
- 将要写入的非强制参数的总数(又称“属性”字节数组大小–供编组者解析)
- “属性”字节数组(供编组员正确分配字段值)
- 实际非强制性参数值
- 对于“ phoneNumbers”字段,我们构造并将其内容的String表示形式写入流中。 或者,我们可以使用“ objectOutput.writeObject(phoneNumbers)”操作序列化整个“ phoneNumbers”对象。 在这种情况下,默认的序列化机制将导致结果流的速度下降和大小增加
- 我们实现了“ readExternal”方法来对“ Employee”对象进行编组。 所有必填字段都将写入流中。 对于非必填字段,demarshaller根据上述协议分配适当的字段值
对于序列化和反序列化过程,我们使用了以下四个功能。 这些功能有两种形式。 第一对适用于序列化和反序列化Externalizable对象实例,而第二对适用于序列化和反序列化Serializable对象实例。
public static byte[][] serializeObject(Externalizable object) throws Exception {
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
byte[][] res = new byte[2][];
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
object.writeExternal(oos);
oos.flush();
res[0] = object.getClass().getName().getBytes();
res[1] = baos.toByteArray();
} catch (Exception ex) {
throw ex;
} finally {
try {
if(oos != null)
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return res;
}
public static Externalizable deserializeObject(byte[][] rowObject) throws Exception {
ObjectInputStream ois = null;
String objectClassName = null;
Externalizable res = null;
try {
objectClassName = new String(rowObject[0]);
byte[] objectBytes = rowObject[1];
ois = new ObjectInputStream(new ByteArrayInputStream(objectBytes));
Class objectClass = Class.forName(objectClassName);
res = (Externalizable) objectClass.newInstance();
res.readExternal(ois);
} catch (Exception ex) {
throw ex;
} finally {
try {
if(ois != null)
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return res;
}
public static byte[] serializeObject(Serializable object) throws Exception {
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
byte[] res = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
oos.flush();
res = baos.toByteArray();
} catch (Exception ex) {
throw ex;
} finally {
try {
if(oos != null)
oos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return res;
}
public static Serializable deserializeObject(byte[] rowObject) throws Exception {
ObjectInputStream ois = null;
Serializable res = null;
try {
ois = new ObjectInputStream(new ByteArrayInputStream(rowObject));
res = (Serializable) ois.readObject();
} catch (Exception ex) {
throw ex;
} finally {
try {
if(ois != null)
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return res;
}
下面我们展示了上述两种方法之间的性能比较表
横轴表示测试运行的次数,纵轴表示每次测试运行的每秒平均事务数(TPS)。 因此,较高的值更好。 如您所见,与普通的Serializable方法相比,使用Externalizable方法可以在序列化和反序列化时获得出色的性能提升。
最后,我们必须指出我们执行了测试,为“ Employee”对象的所有非必填字段提供了值。 如果在相同方法之间进行比较时不使用所有非强制性参数,并且最重要的是在Externalizable和Serializable方法之间进行交叉比较时,您应该期望获得更高的性能提升。
编码愉快!
贾斯汀
- Java最佳实践–多线程环境中的DateFormat
- Java最佳实践– Vector vs ArrayList vs HashSet
- Java最佳实践–字符串性能和精确字符串匹配
- Java最佳实践–队列之战和链接的ConcurrentHashMap
- Java最佳实践– Char到Byte和Byte到Char的转换
翻译自: https://www.javacodegeeks.com/2010/07/java-best-practices-high-performance.html