Java效能優化技巧集合

iteye_15146發表於2010-03-05
=== ================================摘要: ===================================
可供程式利用的資源(記憶體、CPU時間、網路頻寬等)是有限的,優化的目的就是讓程式用盡可能少的資源完成預定的任務。優化通常包含兩方面的內容:減小程式碼的體積,提高程式碼的執行效率。本文討論的主要是如何提高程式碼的效率。 === ================================提綱: ===================================

一、通用篇  

1.1不用new關鍵詞建立類的例項  
1.2使用非阻塞I / O  
1.3慎用異常  
1.4不要重複初始化變數  
1.5儘量指定類的final修飾符  
1.6儘量使用區域性變數  
1.7乘法和除法
二、J2EE篇  
2.1使用緩衝標記  
2.2始終通過會話Bean訪問實體Bean  
2.3選擇合適的引用機制  
2.4在部署描述器中設定只讀屬性  
2.5緩衝對EJB Home的訪問  
2.6為EJB實現本地介面  
2.7生成主鍵  
2.8及時清除不再需要的會話  
2.9在JSP頁面中關閉無用的會話  
2.10 Servlet與記憶體使用  
2.11 HTTP Keep - Alive  
2.12 JDBC與Unicode  
2.13 JDBC與I / O  
2.14記憶體資料庫

三、GUI篇  
3.1用JAR壓縮類檔案  
3.2提示Applet裝入程式  
3.3在畫出圖形之前預先裝入它  
3.4覆蓋update方法  
3.5延遲重畫操作  
3.6使用雙緩衝區  
3.7使用BufferedImage  
3.8使用VolatileImage  
3.9使用Window Blitting

四、補充資料

=== ================================正文: ===================================

一、通用篇

“通用篇”討論的問題適合於大多數Java應用。

1.1不用new關鍵詞建立類的例項

  用new關鍵詞建立類的例項時,建構函式鏈中的所有建構函式都會被自動呼叫。但如果一個物件實現了Cloneable介面,我們可以呼叫它的clone()方法。clone()方法不會呼叫任何類建構函式。

  在使用設計模式(Design Pattern)的場合,如果用Factory模式建立物件,則改用clone()方法建立新的物件例項非常簡單。例如,下面是Factory模式的一個典型實現:

原始碼複製列印
public static Credit getNewCredit() {   
return new Credit();
}
public static Credit getNewCredit() {
return new Credit();
}

  改進後的程式碼使用clone()方法,如下所示:

原始碼複製列印
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}

  上面的思路對於陣列處理同樣很有用。

1.2使用非阻塞I / O

  版本較低的JDK不支援非阻塞I / O API。為避免I / O阻塞,一些應用採用了建立大量執行緒的辦法(在較好的情況下,會使用一個緩衝池)。這種技術可以在許多必須支援併發I / O流的應用中見到,如Web伺服器、報價和拍賣應用等。然而,建立Java執行緒需要相當可觀的開銷。

  JDK 1.4引入了非阻塞的I / O庫(java.nio)。如果應用要求使用版本較早的JDK,在這裡有一個支援非阻塞I / O的軟體包。

  請參見Sun中國網站的《調整Java的I / O效能》。

1.3慎用異常

  異常對效能不利。丟擲異常首先要建立一個新的物件。Throwable介面的建構函式呼叫名為fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆疊,收集呼叫跟蹤資訊。只要有異常被丟擲,VM就必須調整呼叫堆疊,因為在處理過程中建立了一個新的物件。

  異常只能用於錯誤處理,不應該用來控制程式流程。

1.4不要重複初始化變數

  預設情況下,呼叫類的建構函式時,Java會把變數初始化成確定的值:所有的物件被設定成null,整數變數(byte、short、int、long)設定成0,float和double變數設定成0.0,邏輯值設定成false。當一個類從另一個類派生時,這一點尤其應該注意,因為用new關鍵詞建立一個物件時,建構函式鏈中的所有建構函式都會被自動呼叫。

1.5儘量指定類的final修飾符

  帶有final修飾符的類是不可派生的。在Java核心API中,有許多應用final的例子,例如java.lang.String。為String類指定final防止了人們覆蓋length()方法。

  另外,如果指定一個類為final,則該類所有的方法都是final。Java編譯器會尋找機會內聯(inline)所有的final方法(這和具體的編譯器實現有關)。此舉能夠使效能平均提高50 % 。

1.6儘量使用區域性變數

  呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧(Stack)中,速度較快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢。另外,依賴於具體的編譯器 / JVM,區域性變數還可能得到進一步優化。請參見《儘可能使用堆疊變數》。

1.7乘法和除法

  考慮下面的程式碼:

原始碼複製列印
for (val = 0; val < 100000; val += 5) {   
alterX = val * 8;
myResult = val * 2;
}
for (val = 0; val < 100000; val += 5) {
alterX = val * 8;
myResult = val * 2;
}

  用移位操作替代乘法操作可以極大地提高效能。下面是修改後的程式碼:

原始碼複製列印
for (val = 0; val < 100000; val += 5) {   
alterX = val << 3;
myResult = val << 1;
}
for (val = 0; val < 100000; val += 5) {
alterX = val << 3;
myResult = val << 1;
}

  修改後的程式碼不再做乘以8的操作,而是改用等價的左移3位操作,每左移1位相當於乘以2。相應地,右移1位操作相當於除以2。值得一提的是,雖然移位操作速度快,但可能使程式碼比較難於理解,所以最好加上一些註釋。

二、J2EE篇

  前面介紹的改善效能技巧適合於大多數Java應用,接下來要討論的問題適合於使用JSP、EJB或JDBC的應用。

2.1使用緩衝標記

  一些應用伺服器加入了面向JSP的緩衝標記功能。例如,BEA的WebLogic Server從6.0版本開始支援這個功能,Open Symphony工程也同樣支援這個功能。JSP緩衝標記既能夠緩衝頁面片斷,也能夠緩衝整個頁面。當JSP頁面執行時,如果目標片斷已經在緩衝之中,則生成該片斷的程式碼就不用再執行。頁面級緩衝捕獲對指定URL的請求,並緩衝整個結果頁面。對於購物籃、目錄以及入口網站的主頁來說,這個功能極其有用。對於這類應用,頁面級緩衝能夠儲存頁面執行的結果,供後繼請求使用。

  對於程式碼邏輯複雜的頁面,利用緩衝標記提高效能的效果比較明顯;反之,效果可能略遜一籌。

  請參見《用緩衝技術提高JSP應用的效能和穩定性》。

2.2始終通過會話Bean訪問實體Bean

  直接訪問實體Bean不利於效能。當客戶程式遠端訪問實體Bean時,每一個get方法都是一個遠端呼叫。訪問實體Bean的會話Bean是本地的,能夠把所有資料組織成一個結構,然後返回它的值。

  用會話Bean封裝對實體Bean的訪問能夠改進事務管理,因為會話Bean只有在到達事務邊界時才會提交。每一個對get方法的直接呼叫產生一個事務,容器將在每一個實體Bean的事務之後執行一個“裝入 - 讀取”操作。

  一些時候,使用實體Bean會導致程式效能不佳。如果實體Bean的唯一用途就是提取和更新資料,改成在會話Bean之內利用JDBC訪問資料庫可以得到更好的效能。

2.3選擇合適的引用機制

  在典型的JSP應用系統中,頁頭、頁尾部分往往被抽取出來,然後根據需要引入頁頭、頁尾。當前,在JSP頁面中引入外部資源的方法主要有兩種:include指令,以及include動作。

  include指令:例如 < %@include file = "copyright.html" % >。該指令在編譯時引入指定的資源。在編譯之前,帶有include指令的頁面和指定的資源被合併成一個檔案。被引用的外部資源在編譯時就確定,比執行時才確定資源更高效。  

  include動作:例如 < jsp: include page = "copyright.jsp" / >。該動作引入指定頁面執行後生成的結果。由於它在執行時完成,因此對輸出結果的控制更加靈活。但時,只有當被引用的內容頻繁地改變時,或者在對主頁面的請求沒有出現之前,被引用的頁面無法確定時,使用include動作才合算。2.4在部署描述器中設定只讀屬性

  實體Bean的部署描述器允許把所有get方法設定成“只讀”。當某個事務單元的工作只包含執行讀取操作的方法時,設定只讀屬性有利於提高效能,因為容器不必再執行儲存操作。

2.5緩衝對EJB Home的訪問

  EJB Home介面通過JNDI名稱查詢獲得。這個操作需要相當可觀的開銷。JNDI查詢最好放入Servlet的init()方法裡面。如果應用中多處頻繁地出現EJB訪問,最好建立一個EJBHomeCache類。EJBHomeCache類一般應該作為singleton實現。

2.6為EJB實現本地介面

  本地介面是EJB 2.0規範新增的內容,它使得Bean能夠避免遠端呼叫的開銷。請考慮下面的程式碼。

  
PayBeanHome home = (PayBeanHome) javax.rmi.PortableRemoteObject.narrow(ctx.lookup("PayBeanHome"), PayBeanHome.class);
  PayBean bean = (PayBean) javax.rmi.PortableRemoteObject.narrow(home.create(), PayBean.class);

  第一個語句表示我們要尋找Bean的Home介面。這個查詢通過JNDI進行,它是一個RMI呼叫。然後,我們定位遠端物件,返回代理引用,這也是一個RMI呼叫。第二個語句示範瞭如何建立一個例項,涉及了建立IIOP請求並在網路上傳輸請求的stub程式,它也是一個RMI呼叫。

  要實現本地介面,我們必須作如下修改:

  方法不能再丟擲java.rmi.RemoteException異常,包括從RemoteException派生的異常,比如TransactionRequiredException、TransactionRolledBackException和NoSuchObjectException。

  EJB提供了等價的本地異常,如TransactionRequiredLocalException、TransactionRolledBackLocalException和NoSuchObjectLocalException。所有資料和返回值都通過引用的方式傳遞,而不是傳遞值。本地介面必須在EJB部署的機器上使用。簡而言之,客戶程式和提供服務的元件必須在同一個JVM上執行。如果Bean實現了本地介面,則其引用不可序列化。請參見《用本地引用提高EJB訪問效率》。

  2.7生成主鍵

  在EJB之內生成主鍵有許多途徑,下面分析了幾種常見的辦法以及它們的特點。

  利用資料庫內建的標識機制(SQL Server的IDENTITY或Oracle的SEQUENCE)。這種方法的缺點是EJB可移植性差。由實體Bean自己計算主鍵值(比如做增量操作)。它的缺點是要求事務可序列化,而且速度也較慢。

  利用NTP之類的時鐘服務。這要求有面向特定平臺的原生程式碼,從而把Bean固定到了特定的OS之上。另外,它還導致了這樣一種可能,即在多CPU的伺服器上,同一個毫秒之內生成了兩個主鍵。

  借鑑Microsoft的思路,在Bean中建立一個GUID。然而,如果不求助於JNI,Java不能確定網路卡的MAC地址;如果使用JNI,則程式就要依賴於特定的OS。

  還有其他幾種辦法,但這些辦法同樣都有各自的侷限。似乎只有一個答案比較理想:結合運用RMI和JNDI。先通過RMI註冊把RMI遠端物件繫結到JNDI樹。客戶程式通過JNDI進行查詢。下面是一個例子:

原始碼複製列印
public class keyGenerator extends UnicastRemoteObject implements Remote {   
private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey() throws RemoteException {
return KeyValue++;
}
public class keyGenerator extends UnicastRemoteObject implements Remote {
private static long KeyValue = System.currentTimeMillis();
public static synchronized long getKey() throws RemoteException {
return KeyValue++;
}

2.8及時清除不再需要的會話

  為了清除不再活動的會話,許多應用伺服器都有預設的會話超時時間,一般為30分鐘。當應用伺服器需要儲存更多會話時,如果記憶體容量不足,作業系統會把部分記憶體資料轉移到磁碟,應用伺服器也可能根據“最近最頻繁使用”(Most Recently Used)演算法把部分不活躍的會話轉儲到磁碟,甚至可能丟擲“記憶體不足”異常。在大規模系統中,序列化會話的代價是很昂貴的。當會話不再需要時,應當及時呼叫HttpSession.invalidate()方法清除會話。HttpSession.invalidate()方法通常可以在應用的退出頁面呼叫。

2.9在JSP頁面中關閉無用的會話

對於那些無需跟蹤會話狀態的頁面,關閉自動建立的會話可以節省一些資源。使用如下page指令:

< %@page session = "false" % >

  2.10 Servlet與記憶體使用

許多開發者隨意地把大量資訊儲存到使用者會話之中。一些時候,儲存在會話中的物件沒有及時地被垃圾回收機制回收。從效能上看,典型的症狀是使用者感到系統週期性地變慢,卻又不能把原因歸於任何一個具體的元件。如果監視JVM的堆空間,它的表現是記憶體佔用不正常地大起大落。

解決這類記憶體問題主要有二種辦法。第一種辦法是,在所有作用範圍為會話的Bean中實現HttpSessionBindingListener介面。這樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean使用的資源。

另外一種辦法就是儘快地把會話作廢。大多數應用伺服器都有設定會話作廢間隔時間的選項。另外,也可以用程式設計的方式呼叫會話的setMaxInactiveInterval()方法,該方法用來設定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計。

  2.11 HTTP Keep - Alive

Keep - Alive功能使客戶端到伺服器端的連線持續有效,當出現對伺服器的後繼請求時,Keep - Alive功能避免了建立或者重新建立連線。市場上的大部分Web伺服器,包括iPlanet、IIS和Apache,都支援HTTP Keep - Alive。對於提供靜態內容的網站來說,這個功能通常很有用。但是,對於負擔較重的網站來說,這裡存在另外一個問題:雖然為客戶保留開啟的連線有一定的好處,但它同樣影響了效能,因為在處理暫停期間,本來可以釋放的資源仍舊被佔用。當Web伺服器和應用伺服器在同一臺機器上執行時,Keep - Alive功能對資源利用的影響尤其突出。

2.12 JDBC與Unicode
想必你已經瞭解一些使用JDBC時提高效能的措施,比如利用連線池、正確地選擇儲存過程和直接執行的SQL、從結果集刪除多餘的列、預先編譯SQL語句,等等。

除了這些顯而易見的選擇之外,另一個提高效能的好選擇可能就是把所有的字元資料都儲存為Unicode(內碼表13488)。Java以Unicode形式處理所有資料,因此,資料庫驅動程式不必再執行轉換過程。但應該記住:如果採用這種方式,資料庫會變得更大,因為每個Unicode字元需要2個位元組儲存空間。另外,如果有其他非Unicode的程式訪問資料庫,效能問題仍舊會出現,因為這時資料庫驅動程式仍舊必須執行轉換過程。

2.13 JDBC與I / O

如果應用程式需要訪問一個規模很大的資料集,則應當考慮使用塊提取方式。預設情況下,JDBC每次提取32行資料。舉例來說,假設我們要遍歷一個5000行的記錄集,JDBC必須呼叫資料庫157次才能提取到全部資料。如果把塊大小改成512,則呼叫資料庫的次數將減少到10次。

在一些情形下這種技術無效。例如,如果使用可滾動的記錄集,或者在查詢中指定了FOR UPDATE,則塊操作方式不再有效。

   1.14記憶體資料庫

許多應用需要以使用者為單位在會話物件中儲存相當數量的資料,典型的應用如購物籃和目錄等。由於這類資料可以按照行 / 列的形式組織,因此,許多應用建立了龐大的Vector或HashMap。在會話中儲存這類資料極大地限制了應用的可伸縮性,因為伺服器擁有的記憶體至少必須達到每個會話佔用的記憶體數量乘以併發使用者最大數量,它不僅使伺服器價格昂貴,而且垃圾收集的時間間隔也可能延長到難以忍受的程度。

一些人把購物籃 / 目錄功能轉移到資料庫層,在一定程度上提高了可伸縮性。然而,把這部分功能放到資料庫層也存在問題,且問題的根源與大多數關聯式資料庫系統的體系結構有關。對於關聯式資料庫來說,執行時的重要原則之一是確保所有的寫入操作穩定、可靠,因而,所有的效能問題都與物理上把資料寫入磁碟的能力有關。關聯式資料庫力圖減少I / O操作,特別是對於讀操作,但實現該目標的主要途徑只是執行一套實現緩衝機制的複雜演算法,而這正是資料庫層第一號效能瓶頸通常總是CPU的主要原因。

一種替代傳統關聯式資料庫的方案是,使用在記憶體中執行的資料庫(In - memory Database),例如TimesTen。記憶體資料庫的出發點是允許資料臨時地寫入,但這些資料不必永久地儲存到磁碟上,所有的操作都在記憶體中進行。這樣,記憶體資料庫不需要複雜的演算法來減少I / O操作,而且可以採用比較簡單的加鎖機制,因而速度很快。

三、GUI篇

這一部分介紹的內容適合於圖形使用者介面的應用(Applet和普通應用),要用到AWT或Swing。

3.1用JAR壓縮類檔案

Java檔案檔案(JAR檔案)是根據JavaBean標準壓縮的檔案,是釋出JavaBean元件的主要方式和推薦方式。JAR檔案有助於減少檔案體積,縮短下載時間。例如,它有助於Applet提高啟動速度。一個JAR檔案可以包含一個或者多個相關的Bean以及支援檔案,比如圖形、聲音、HTML和其他資源。

要在HTML / JSP檔案中指定JAR檔案,只需在Applet標記中加入ARCHIVE = "name.jar"宣告。

請參見《使用檔案檔案提高applet的載入速度》。

  3.2提示Applet裝入程式

你是否看到過使用Applet的網站,注意到在應該執行Applet的地方出現了一個佔位符?當Applet的下載時間較長時,會發生什麼事情?最大的可能就是使用者掉頭離去。在這種情況下,顯示一個Applet正在下載的資訊無疑有助於鼓勵使用者繼續等待。

下面我們來看看一種具體的實現方法。首先建立一個很小的Applet,該Applet負責在後臺下載正式的Applet:
原始碼複製列印
import java.applet.Applet;   
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable,
AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求裝載的正式Applet
largeAppletName = getParameter("applet");
// “請稍等”提示資訊
label = new Label("請稍等..." + largeAppletName);
add(label);
}
public void run() {
try {
// 獲得待裝載Applet的類
Class largeAppletClass = Class.forName(largeAppletName);
// 建立待裝載Applet的例項
Applet largeApplet = (Applet) largeAppletClass.newInstance();
// 設定該Applet的Stub程式
largeApplet.setStub(this);
// 取消“請稍等”資訊
remove(label);
// 設定佈局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 顯示正式的Applet
largeApplet.init();
largeApplet.start();
} catch(Exception ex) {
// 顯示錯誤資訊
label.setText("不能裝入指定的Applet");
}
// 重新整理螢幕
validate();
}
public void appletResize(int width, int height) {
// 把appletResize呼叫從stub程式傳遞到Applet
resize(width, height);
}
}
import java.applet.Applet;
import java.applet.AppletStub;
import java.awt.Label;
import java.awt.Graphics;
import java.awt.GridLayout;
public class PreLoader extends Applet implements Runnable,
AppletStub {
String largeAppletName;
Label label;
public void init() {
// 要求裝載的正式Applet
largeAppletName = getParameter("applet");
// “請稍等”提示資訊
label = new Label("請稍等..." + largeAppletName);
add(label);
}
public void run() {
try {
// 獲得待裝載Applet的類
Class largeAppletClass = Class.forName(largeAppletName);
// 建立待裝載Applet的例項
Applet largeApplet = (Applet) largeAppletClass.newInstance();
// 設定該Applet的Stub程式
largeApplet.setStub(this);
// 取消“請稍等”資訊
remove(label);
// 設定佈局
setLayout(new GridLayout(1, 0));
add(largeApplet);
// 顯示正式的Applet
largeApplet.init();
largeApplet.start();
} catch(Exception ex) {
// 顯示錯誤資訊
label.setText("不能裝入指定的Applet");
}
// 重新整理螢幕
validate();
}
public void appletResize(int width, int height) {
// 把appletResize呼叫從stub程式傳遞到Applet
resize(width, height);
}
}

編譯後的程式碼小於2K,下載速度很快。程式碼中有幾個地方值得注意。首先,PreLoader實現了AppletStub介面。一般地,Applet從呼叫者判斷自己的codebase。在本例中,我們必須呼叫setStub()告訴Applet到哪裡提取這個資訊。另一個值得注意的地方是,AppletStub介面包含許多和Applet類一樣的方法,但appletResize()方法除外。這裡我們把對appletResize()方法的呼叫傳遞給了resize()方法。

  3.3在畫出圖形之前預先裝入它

ImageObserver介面可用來接收圖形裝入的提示資訊。ImageObserver介面只有一個方法imageUpdate(),能夠用一次repaint()操作在螢幕上畫出圖形。下面提供了一個例子。

原始碼複製列印
public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
if ((flags & ALLBITS) != 0 {
repaint();
} else if (flags & (ERROR | ABORT)) != 0) {
error = true;
// 檔案沒有找到,考慮顯示一個佔位符
repaint();
}
return (flags & (ALLBITS | ERROR | ABORT)) == 0;
}
public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) {
if ((flags & ALLBITS) != 0 {
repaint();
} else if (flags & (ERROR | ABORT)) != 0) {
error = true;
// 檔案沒有找到,考慮顯示一個佔位符
repaint();
}
return (flags & (ALLBITS | ERROR | ABORT)) == 0;
}

當圖形資訊可用時,imageUpdate()方法被呼叫。如果需要進一步更新,該方法返回true;如果所需資訊已經得到,該方法返回false。

  3.4覆蓋update方法

update()方法的預設動作是清除螢幕,然後呼叫paint()方法。如果使用預設的update()方法,頻繁使用圖形的應用可能出現顯示閃爍現象。要避免在paint()呼叫之前的螢幕清除操作,只需按照如下方式覆蓋update()方法:

原始碼複製列印
public void update(Graphics g) {
paint(g);
}

更理想的方案是:覆蓋update(),只重畫螢幕上發生變化的區域,如下所示:

public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}
public void update(Graphics g) {
paint(g);
}

更理想的方案是:覆蓋update(),只重畫螢幕上發生變化的區域,如下所示:

public void update(Graphics g) {
g.clipRect(x, y, w, h);
paint(g);
}

  3.5延遲重畫操作

對於圖形使用者介面的應用來說,效能低下的主要原因往往可以歸結為重畫螢幕的效率低下。當使用者改變視窗大小或者滾動一個視窗時,這一點通常可以很明顯地觀察到。改變視窗大小或者滾動螢幕之類的操作導致重畫螢幕事件大量地、快速地生成,甚至超過了相關程式碼的執行速度。對付這個問題最好的辦法是忽略所有“遲到”的事件。

建議在這裡引入一個數毫秒的時差,即如果我們立即接收到了另一個重畫事件,可以停止處理當前事件轉而處理最後一個收到的重畫事件;否則,我們繼續進行當前的重畫過程。

如果事件要啟動一項耗時的工作,分離出一個工作執行緒是一種較好的處理方式;否則,一些部件可能被“凍結”,因為每次只能處理一個事件。下面提供了一個事件處理的簡單例子,但經過擴充套件後它可以用來控制工作執行緒。

原始碼複製列印
public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) { // e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 獲得當前時間
lastOne.time = time; (new Thread() {
public void run() {
if (milliseconds > 0) {
try {
Thread.sleep(milliseconds);
} // 暫停執行緒
catch(Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件結束
if (lastOne.time != time) // 只處理最後一個事件
return;
}
}
}).start();
}
private static Hashtable e_queue = new Hashtable();
private static class LastOne {
public long time = 0;
public Object running = new Object();
}
public static void runOnce(String id, final long milliseconds) {
synchronized(e_queue) { // e_queue: 所有事件的集合
if (!e_queue.containsKey(id)) {
e_queue.put(token, new LastOne());
}
}
final LastOne lastOne = (LastOne) e_queue.get(token);
final long time = System.currentTimeMillis(); // 獲得當前時間
lastOne.time = time; (new Thread() {
public void run() {
if (milliseconds > 0) {
try {
Thread.sleep(milliseconds);
} // 暫停執行緒
catch(Exception ex) {}
}
synchronized(lastOne.running) { // 等待上一事件結束
if (lastOne.time != time) // 只處理最後一個事件
return;
}
}
}).start();
}
private static Hashtable e_queue = new Hashtable();
private static class LastOne {
public long time = 0;
public Object running = new Object();
}

  3.6使用雙緩衝區

在螢幕之外的緩衝區繪圖,完成後立即把整個圖形顯示出來。由於有兩個緩衝區,所以程式可以來回切換。這樣,我們可以用一個低優先順序的執行緒負責畫圖,使得程式能夠利用空閒的CPU時間執行其他任務。下面的虛擬碼片斷示範了這種技術。

原始碼複製列印
Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);
Graphics myGraphics;
Image myOffscreenImage = createImage(size().width, size().height);
Graphics offscreenGraphics = myOffscreenImage.getGraphics();
offscreenGraphics.drawImage(img, 50, 50, this);
myGraphics.drawImage(myOffscreenImage, 0, 0, this);

  3.7使用BufferedImage

Java JDK 1.2使用了一個軟顯示裝置,使得文字在不同的平臺上看起來相似。為實現這個功能,Java必須直接處理構成文字的畫素。由於這種技術要在記憶體中大量地進行位複製操作,早期的JDK在使用這種技術時效能不佳。為解決這個問題而提出的Java標準實現了一種新的圖形型別,即BufferedImage。

BufferedImage子類描述的圖形帶有一個可訪問的圖形資料緩衝區。一個BufferedImage包含一個ColorModel和一組光柵圖形資料。這個類一般使用RGB(紅、綠、藍)顏色模型,但也可以處理灰度級圖形。它的建構函式很簡單,如下所示:

public BufferedImage(int width, int height, int imageType)

ImageType允許我們指定要緩衝的是什麼型別的圖形,比如5 - 位RGB、8 - 位RGB、灰度級等。

  3.8使用VolatileImage

許多硬體平臺和它們的作業系統都提供基本的硬體加速支援。例如,硬體加速一般提供矩形填充功能,和利用CPU完成同一任務相比,硬體加速的效率更高。由於硬體加速分離了一部分工作,允許多個工作流併發進行,從而緩解了對CPU和系統匯流排的壓力,使得應用能夠執行得更快。利用VolatileImage可以建立硬體加速的圖形以及管理圖形的內容。由於它直接利用低層平臺的能力,效能的改善程度主要取決於系統使用的圖形介面卡。VolatileImage的內容隨時可能丟失,也即它是“不穩定的(volatile)”。因此,在使用圖形之前,最好檢查一下它的內容是否丟失。VolatileImage有兩個能夠檢查內容是否丟失的方法:

public abstract int validate(GraphicsConfiguration gc);
public abstract Boolean contentsLost();

每次從VolatileImage物件複製內容或者寫入VolatileImage時,應該呼叫validate()方法。contentsLost()方法告訴我們,自從最後一次validate()呼叫之後,圖形的內容是否丟失。

雖然VolatileImage是一個抽象類,但不要從它這裡派生子類。VolatileImage應該通過Component.createVolatileImage()或者GraphicsConfiguration.createCompatibleVolatileImage()方法建立。

  3.9使用Window Blitting

進行滾動操作時,所有可見的內容一般都要重畫,從而導致大量不必要的重畫工作。許多作業系統的圖形子系統,包括WIN32 GDI、MacOS和X / Windows,都支援Window Blitting技術。Window Blitting技術直接在螢幕緩衝區中把圖形移到新的位置,只重畫新出現的區域。要在Swing應用中使用Window Blitting技術,設定方法如下:

setScrollMode(int mode);

在大多數應用中,使用這種技術能夠提高滾動速度。只有在一種情形下,Window Blitting會導致效能降低,即應用在後臺進行滾動操作。如果是使用者在滾動一個應用,那麼它總是在前臺,無需擔心任何負面影響。

   四、補充資料

CCW:Java效能的優化(上),下CCIDNet:實用EJB開發技巧Sun中國:Java HotSpot效能引擎的體系結構Sun中國:對Java HotSpot效能引擎的深入研究Sun中國:JAVA效能技巧和防火牆隧道技術IBM DeveloperWorks中國:Java技巧86:JDK1.3中的本地繪製支援IBM DeveloperWorks中國:Java技巧90:加快GUI的速度IBM DeveloperWorks中國:編寫高效的執行緒安全類IBM DeveloperWorks中國:連線池:深入J2EE的連線合用Design
for performance Java Performance Tuning Strategy JavaWorld:Performance Tuning

相關文章