在使用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