舉一個(gè)例子,我們做項目需要安排計劃,每一個(gè)模塊可以由多人同時(shí)并行做多項任務(wù),也可以一個(gè)人或者多個(gè)人串行工作,但始終會(huì )有一條關(guān)鍵路徑,這條路徑就是項目的工期。系統一次調用的響應時(shí)間跟項目計劃一樣,也有一條關(guān)鍵路徑,這個(gè)關(guān)鍵路徑是就是系統影響時(shí)間。關(guān)鍵路徑由 CPU 運算、IO、外部系統響應等等組成。
對于一個(gè)系統的用戶(hù)來(lái)說(shuō),從用戶(hù)點(diǎn)擊一個(gè)按鈕、鏈接或發(fā)出一條指令開(kāi)始,到系統把結果以用戶(hù)希望的形式展現出來(lái)為終止,整個(gè)過(guò)程所消耗的時(shí)間是用戶(hù)對這個(gè)軟件性能的直觀(guān)印象,也就是我們所說(shuō)的響應時(shí)間。當響應時(shí)間較短時(shí),用戶(hù)體驗是很好的,當然用戶(hù)體驗的響應時(shí)間包括個(gè)人主觀(guān)因素和客觀(guān)響應時(shí)間。在設計軟件時(shí),我們就需要考慮到如何更好地結合這兩部分達到用戶(hù)最佳的體驗。如:用戶(hù)在大數據量查詢(xún)時(shí),我們可以將先提取出來(lái)的數據展示給用戶(hù),在用戶(hù)看的過(guò)程中繼續進(jìn)行數據檢索,這時(shí)用戶(hù)并不知道我們后臺在做什么,用戶(hù)關(guān)注的是用戶(hù)操作的響應時(shí)間。
我們經(jīng)常說(shuō)的一個(gè)系統吞吐量,通常由 QPS(TPS)、并發(fā)數兩個(gè)因素決定,每套系統這兩個(gè)值都有一個(gè)相對極限值,在應用場(chǎng)景訪(fǎng)問(wèn)壓力下,只要某一項達到系統最高值,系統的吞吐量就上不去了,如果壓力繼續增大,系統的吞吐量反而會(huì )下降,原因是系統超負荷工作,上下文切換、內存等等其它消耗導致系統性能下降,決定系統響應時(shí)間要素。
緩沖區是一塊特定的內存區域,開(kāi)辟緩沖區的目的是通過(guò)緩解應用程序上下層之間的性能差異,提高系統的性能。在日常生活中,緩沖的一個(gè)典型應用是漏斗。緩沖可以協(xié)調上層組件和下層組件的性能差,當上層組件性能優(yōu)于下層組件時(shí),可以有效減少上層組件對下層組件的等待時(shí)間?;谶@樣的結構,上層應用組件不需要等待下層組件真實(shí)地接受全部數據,即可返回操作,加快了上層組件的處理速度,從而提升系統整體性能。
BufferedWriter 就是一個(gè)緩沖區用法,一般來(lái)說(shuō),緩沖區不宜過(guò)小,過(guò)小的緩沖區無(wú)法起到真正的緩沖作用,緩沖區也不宜過(guò)大,過(guò)大的緩沖區會(huì )浪費系統內存,增加 GC 負擔。盡量在 I/O 組件內加入緩沖區,可以提高性能。一個(gè)緩沖區例子代碼如清單 1 所示。
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import javax.swing.JApplet;
public class NoBufferMovingCircle extends JApplet implements Runnable{
Image screenImage = null;
Thread thread;
int x = 5;
int move = 1;
public void init(){
screenImage = createImage(230,160);
}
public void start(){
if(thread == null){
thread = new Thread(this);
thread.start();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try{
System.out.println(x);
while(true){
x+=move;
System.out.println(x);
if((x>105)||(x<5)){
move*=-1;
}
repaint();
Thread.sleep(10);
}
}catch(Exception e){
}
}
public void drawCircle(Graphics gc){
Graphics2D g = (Graphics2D) gc;
g.setColor(Color.GREEN);
g.fillRect(0, 0, 200, 100);
g.setColor(Color.red);
g.fillOval(x, 5, 90, 90);
}
public void paint(Graphics g){
g.setColor(Color.white);
g.fillRect(0, 0, 200, 100);
drawCircle(g);
}
}程序可以完成紅球的左右平移,但是效果較差,因為每次的界面刷新都涉及圖片的重新繪制,這較為費時(shí),因此,畫(huà)面的抖動(dòng)和白光效果明顯。為了得到更優(yōu)質(zhì)的顯示效果,可以為它加上緩沖區。代碼如清單 2 所示。
import java.awt.Color;
import java.awt.Graphics;
public class BufferMovingCircle extends NoBufferMovingCircle{
Graphics doubleBuffer = null;//緩沖區
public void init(){
super.init();
doubleBuffer = screenImage.getGraphics();
}
public void paint(Graphics g){//使用緩沖區,優(yōu)化原有的 paint 方法
doubleBuffer.setColor(Color.white);//先在內存中畫(huà)圖
doubleBuffer.fillRect(0, 0, 200, 100);
drawCircle(doubleBuffer);
g.drawImage(screenImage, 0, 0, this);
}
}除 NIO 外,使用 Java 進(jìn)行 I/O 操作有兩種基本方式:
無(wú)論使用哪種方式進(jìn)行文件 I/O,如果能合理地使用緩沖,就能有效地提高 I/O 的性能。
下面顯示了可與 InputStream、OutputStream、Writer 和 Reader 配套使用的緩沖組件。
OutputStream-FileOutputStream-BufferedOutputStream
InputStream-FileInputStream-BufferedInputStream
Writer-FileWriter-BufferedWriter
Reader-FileReader-BufferedReader
使用緩沖組件對文件 I/O 進(jìn)行包裝,可以有效提高文件 I/O 的性能。
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class StreamVSBuffer {
public static void streamMethod() throws IOException{
try {
long start = System.currentTimeMillis();
//請替換成自己的文件
DataOutputStream dos = new DataOutputStream(
new FileOutputStream("C:\\StreamVSBuffertest.txt"));
for(int i=0;i<10000;i++){
dos.writeBytes(String.valueOf(i)+"\r\n");//循環(huán) 1 萬(wàn)次寫(xiě)入數據
}
dos.close();
DataInputStream dis = new DataInputStream(new FileInputStream("C:\\StreamVSBuffertest.txt"));
while(dis.readLine() != null){
}
dis.close();
System.out.println(System.currentTimeMillis() - start);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void bufferMethod() throws IOException{
try {
long start = System.currentTimeMillis();
//請替換成自己的文件
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(
new FileOutputStream("C:\\StreamVSBuffertest.txt")));
for(int i=0;i<10000;i++){
dos.writeBytes(String.valueOf(i)+"\r\n");//循環(huán) 1 萬(wàn)次寫(xiě)入數據
}
dos.close();
DataInputStream dis = new DataInputStream(new BufferedInputStream(
new FileInputStream("C:\\StreamVSBuffertest.txt")));
while(dis.readLine() != null){
}
dis.close();
System.out.println(System.currentTimeMillis() - start);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args){
try {
StreamVSBuffer.streamMethod();
StreamVSBuffer.bufferMethod();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}運行結果如清單 4 所示。
889 31
很明顯使用緩沖的代碼性能比沒(méi)有使用緩沖的快了很多倍。清單 5 所示代碼對 FileWriter 和 FileReader 進(jìn)行了相似的測試。
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class WriterVSBuffer {
public static void streamMethod() throws IOException{
try {
long start = System.currentTimeMillis();
FileWriter fw = new FileWriter("C:\\StreamVSBuffertest.txt");//請替換成自己的文件
for(int i=0;i<10000;i++){
fw.write(String.valueOf(i)+"\r\n");//循環(huán) 1 萬(wàn)次寫(xiě)入數據
}
fw.close();
FileReader fr = new FileReader("C:\\StreamVSBuffertest.txt");
while(fr.ready() != false){
}
fr.close();
System.out.println(System.currentTimeMillis() - start);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void bufferMethod() throws IOException{
try {
long start = System.currentTimeMillis();
BufferedWriter fw = new BufferedWriter(new FileWriter("C:\\StreamVSBuffertest.txt"));//請替換成自己的文件
for(int i=0;i<10000;i++){
fw.write(String.valueOf(i)+"\r\n");//循環(huán) 1 萬(wàn)次寫(xiě)入數據
}
fw.close();
BufferedReader fr = new BufferedReader(new FileReader("C:\\StreamVSBuffertest.txt"));
while(fr.ready() != false){
}
fr.close();
System.out.println(System.currentTimeMillis() - start);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args){
try {
StreamVSBuffer.streamMethod();
StreamVSBuffer.bufferMethod();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}運行輸出如清單 6 所示。
1295 31
從上面例子可以看出,無(wú)論對于讀取還是寫(xiě)入文件,適當地使用緩沖,可以有效地提升系統的文件讀寫(xiě)性能,為用戶(hù)減少響應時(shí)間。
緩存也是一塊為提升系統性能而開(kāi)辟的內存空間。緩存的主要作用是暫存數據處理結果,并提供下次訪(fǎng)問(wèn)使用。在很多場(chǎng)合,數據的處理或者數據獲取可能會(huì )非常費時(shí),當對這個(gè)數據的請求量很大時(shí),頻繁的數據處理會(huì )耗盡 CPU 資源。緩存的作用就是將這些來(lái)之不易的數據處理結果暫存起來(lái),當有其他線(xiàn)程或者客戶(hù)端需要查詢(xún)相同的數據資源時(shí),可以省略對這些數據的處理流程,而直接從緩存中獲取處理結果,并立即返回給請求組件,以此提高系統的響應時(shí)間。
目前有很多基于 Java 的緩存框架,比如 EHCache、OSCache 和 JBossCache 等。EHCache 緩存出自 Hibernate,是其默認的數據緩存解決方案;OSCache 緩存是有 OpenSymphony 設計的,它可以用于緩存任何對象,甚至是緩存部分 JSP 頁(yè)面或者 HTTP 請求;JBossCache 是由 JBoss 開(kāi)發(fā)、可用于 JBoss 集群間數據共享的緩存框架。
以 EHCache 為例,EhCache 的主要特性有:
由于 EhCache 是進(jìn)程中的緩存系統,一旦將應用部署在集群環(huán)境中,每一個(gè)節點(diǎn)維護各自的緩存數據,當某個(gè)節點(diǎn)對緩存數據進(jìn)行更新,這些更新的數據無(wú)法在其它節點(diǎn)中共享,這不僅會(huì )降低節點(diǎn)運行的效率,而且會(huì )導致數據不同步的情況發(fā)生。例如某個(gè)網(wǎng)站采用 A、B 兩個(gè)節點(diǎn)作為集群部署,當 A 節點(diǎn)的緩存更新后,而 B 節點(diǎn)緩存尚未更新就可能出現用戶(hù)在瀏覽頁(yè)面的時(shí)候,一會(huì )是更新后的數據,一會(huì )是尚未更新的數據,盡管我們也可以通過(guò) Session Sticky 技術(shù)來(lái)將用戶(hù)鎖定在某個(gè)節點(diǎn)上,但對于一些交互性比較強或者是非 Web 方式的系統來(lái)說(shuō),Session Sticky 顯然不太適合。所以就需要用到 EhCache 的集群解決方案。清單 7 所示是 EHCache 示例代碼。
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
/**
* 第一步:生成 CacheManager 對象
* 第二步:生成 Cache 對象
* 第三步:向 Cache 對象里添加由 key,value 組成的鍵值對的 Element 元素
* @author mahaibo
*
*/
public class EHCacheDemo{
public static void main(String[] args) {
//指定 ehcache.xml 的位置
String fileName="E:\\1008\\workspace\\ehcachetest\\ehcache.xml";
CacheManager manager = new CacheManager(fileName);
//取出所有的 cacheName
String names[] = manager.getCacheNames();
for(int i=0;i<names.length;i++){
System.out.println(names[i]);
}
//根據 cacheName 生成一個(gè) Cache 對象
//第一種方式:
Cache cache=manager.getCache(names[0]);
//第二種方式,ehcache 里必須有 defaultCache 存在,"test"可以換成任何值
// Cache cache = new Cache("test", 1, true, false, 5, 2);
// manager.addCache(cache);
//向 Cache 對象里添加 Element 元素,Element 元素有 key,value 鍵值對組成
cache.put(new Element("key1","values1"));
Element element = cache.get("key1");
System.out.println(element.getValue());
Object obj = element.getObjectValue();
System.out.println((String)obj);
manager.shutdown();
}
}對象復用池是目前很常用的一種系統優(yōu)化技術(shù)。它的核心思想是,如果一個(gè)類(lèi)被頻繁請求使用,那么不必每次都生成一個(gè)實(shí)例,可以將這個(gè)類(lèi)的一些實(shí)例保存在一個(gè)“池”中,待需要使用的時(shí)候直接從池中獲取。這個(gè)“池”就稱(chēng)為對象池。在實(shí)現細節上,它可能是一個(gè)數組,一個(gè)鏈表或者任何集合類(lèi)。對象池的使用非常廣泛,例如線(xiàn)程池和數據庫連接池。線(xiàn)程池中保存著(zhù)可以被重用的線(xiàn)程對象,當有任務(wù)被提交到線(xiàn)程時(shí),系統并不需要新建線(xiàn)程,而是從池中獲得一個(gè)可用的線(xiàn)程,執行這個(gè)任務(wù)。在任務(wù)結束后,不需要關(guān)閉線(xiàn)程,而將它返回到池中,以便下次繼續使用。由于線(xiàn)程的創(chuàng )建和銷(xiāo)毀是較為費時(shí)的工作,因此,在線(xiàn)程頻繁調度的系統中,線(xiàn)程池可以很好地改善性能。數據庫連接池也是一種特殊的對象池,它用于維護數據庫連接的集合。當系統需要訪(fǎng)問(wèn)數據庫時(shí),不需要重新建立數據庫連接,而可以直接從池中獲??;在數據庫操作完成后,也不關(guān)閉數據庫連接,而是將連接返回到連接池中。由于數據庫連接的創(chuàng )建和銷(xiāo)毀是重量級的操作,因此,避免頻繁進(jìn)行這兩個(gè)操作對改善系統的性能也有積極意義。目前應用較為廣泛的數據庫連接池組件有 C3P0 和 Proxool。
以 C3P0 為例,它是一個(gè)開(kāi)源的 JDBC 連接池,它實(shí)現了數據源和 JNDI 綁定,支持 JDBC3 規范和 JDBC2 的標準擴展。目前使用它的開(kāi)源項目有 Hibernate,Spring 等。如果采用 JNDI 方式配置,如清單 8 所示。
<Resource name="jdbc/dbsource" type="com.mchange.v2.c3p0.ComboPooledDataSource" maxPoolSize="50" minPoolSize="5" acquireIncrement="2" initialPoolSize="10" maxIdleTime="60" factory="org.apache.naming.factory.BeanFactory" user="xxxx" password="xxxx" driverClass="oracle.jdbc.driver.OracleDriver" jdbcUrl="jdbc:oracle:thin:@192.168.x.x:1521:orcl" idleConnectionTestPeriod="10" />
參數說(shuō)明:
如果使用 spring,同時(shí)項目中不使用 JNDI,又不想配置 Hibernate,可以直接將 C3P0 配置到 dataSource 中即可,如清單 9 所示。
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass"><value>oracle.jdbc.driver.OracleDriver</value></property> <property name="jdbcUrl"><value>jdbc:oracle:thin:@localhost:1521:Test</value></property> <property name="user"><value>Kay</value></property> <property name="password"><value>root</value></property> <!--連接池中保留的最小連接數。--> <property name="minPoolSize" value="10" /> <!--連接池中保留的最大連接數。Default: 15 --> <property name="maxPoolSize" value="100" /> <!--最大空閑時(shí)間,1800 秒內未使用則連接被丟棄。若為 0 則永不丟棄。Default: 0 --> <property name="maxIdleTime" value="1800" /> <!--當連接池中的連接耗盡的時(shí)候 c3p0 一次同時(shí)獲取的連接數。Default: 3 --> <property name="acquireIncrement" value="3" /> <property name="maxStatements" value="1000" /> <property name="initialPoolSize" value="10" /> <!--每 60 秒檢查所有連接池中的空閑連接。Default: 0 --> <property name="idleConnectionTestPeriod" value="60" /> <!--定義在從數據庫獲取新連接失敗后重復嘗試的次數。Default: 30 --> <property name="acquireRetryAttempts" value="30" /> <property name="breakAfterAcquireFailure" value="true" /> <property name="testConnectionOnCheckout" value="false" /> </bean>
類(lèi)似的做法存在很多種,用戶(hù)可以自行上網(wǎng)搜索。
計算方式轉換比較出名的是時(shí)間換空間方式,它通常用于嵌入式設備,或者內存、硬盤(pán)空間不足的情況。通過(guò)使用犧牲 CPU 的方式,獲得原本需要更多內存或者硬盤(pán)空間才能完成的工作。
一個(gè)非常簡(jiǎn)單的時(shí)間換空間的算法,實(shí)現了 a、b 兩個(gè)變量的值交換。交換兩個(gè)變量最常用的方法是使用一個(gè)中間變量,而引入額外的變量意味著(zhù)要使用更多的空間。采用下面的方法可以免去中間變量,而達到變量交換的目的,其代價(jià)是引入了更多的 CPU 運算。
a=a+b; b=a-b; a=a-b;
另一個(gè)較為有用的例子是對無(wú)符號整數的支持。在 Java 語(yǔ)言中,不支持無(wú)符號整數,這意味著(zhù)當需要無(wú)符號的 Byte 時(shí),需要使用 Short 代替,這也意味著(zhù)空間的浪費。下面代碼演示了使用位運算模擬無(wú)符號 Byte。雖然在取值和設值過(guò)程中需要更多的 CPU 運算,但是可以大大降低對內存空間的需求。
public class UnsignedByte {
public short getValue(byte i){//將 byte 轉為無(wú)符號的數字
short li = (short)(i & 0xff);
return li;
}
public byte toUnsignedByte(short i){
return (byte)(i & 0xff);//將 short 轉為無(wú)符號 byte
}
public static void main(String[] args){
UnsignedByte ins = new UnsignedByte();
short[] shorts = new short[256];//聲明一個(gè) short 數組
for(int i=0;i<shorts.length;i++){//數組不能超過(guò)無(wú)符號 byte 的上限
shorts[i]=(short)i;
}
byte[] bytes = new byte[256];//使用 byte 數組替代 short 數組
for(int i=0;i<bytes.length;i++){
bytes[i]=ins.toUnsignedByte(shorts[i]);//short 數組的數據存到 byte 數組中
}
for(int i=0;i<bytes.length;i++){
System.out.println(ins.getValue(bytes[i])+" ");//從 byte 數組中取出無(wú)符號的 byte
}
}
}運行輸出如清單 12 所示,篇幅所限,只顯示到 10 為止。
0 1 2 3 4 5 6 7 8 9 10
如果 CPU 的能力較弱,可以采用犧牲空間的方式提高計算能力,實(shí)例代碼如清單 13 所示。
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class SpaceSort {
public static int arrayLen = 1000000;
public static void main(String[] args){
int[] a = new int[arrayLen];
int[] old = new int[arrayLen];
Map<Integer,Object> map = new HashMap<Integer,Object>();
int count = 0;
while(count < a.length){
//初始化數組
int value = (int)(Math.random()*arrayLen*10)+1;
if(map.get(value)==null){
map.put(value, value);
a[count] = value;
count++;
}
}
System.arraycopy(a, 0, old, 0, a.length);//從 a 數組拷貝所有數據到 old 數組
long start = System.currentTimeMillis();
Arrays.sort(a);
System.out.println("Arrays.sort spend:"+(System.currentTimeMillis() - start)+"ms");
System.arraycopy(old, 0, a, 0, old.length);//恢復 原有數據
start = System.currentTimeMillis();
spaceTotime(a);
System.out.println("spaceTotime spend:"+(System.currentTimeMillis() - start)+"ms");
}
public static void spaceTotime(int[] array){
int i = 0;
int max = array[0];
int l = array.length;
for(i=1;i<l;i++){
if(array[i]>max){
max = array[i];
}
}
int[] temp = new int[max+1];
for(i=0;i<l;i++){
temp[array[i]] = array[i];
}
int j = 0;
int max1 = max + 1;
for(i=0;i<max1;i++){
if(temp[i] > 0){
array[j++] = temp[i];
}
}
}
}函數 spaceToTime() 實(shí)現了數組的排序,它不計空間成本,以數組的索引下標來(lái)表示數據大小,因此避免了數字間的相互比較,這是一種典型的以空間換時(shí)間的思路。
應對、處理高吞吐量系統有很多方面可以入手,作者將以系列的方式逐步介紹覆蓋所有領(lǐng)域。本文主要介紹了緩沖區、緩存操作、對象復用池、計算方式轉換等優(yōu)化及建議,從實(shí)際代碼演示入手,對優(yōu)化建議及方案進(jìn)行了驗證。作者始終堅信,沒(méi)有什么優(yōu)化方案是百分百有效的,需要讀者根據實(shí)際情況進(jìn)行選擇、實(shí)踐。
聯(lián)系客服