一、ConcurrentHashMap 陷阱:
以下代碼使用Watcher來記錄特定Key的調用次數,考慮到線程安全,使用了ConcurrentHashMap, 這段代碼真的能實作線程安全嗎?
public class Watcher
{
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void add(String key)
{
Integer value = map.get(key);
if(value == null)
{
map.put(key, 1);
}
else
{
map.put(key, value + 1);
}
}
}
答案:這個代碼片段是線程非安全的,比如 A、B 兩個線程同時調用 add("newKey")方法,如下表所示。
T4 時刻時,線程 B 覆寫了線程 A 在 T3 時刻的值,系統調用了 add("newKey")兩次,但“newKey”的值為 1。
以下是一個正确的代碼片段:
private ConcurrentHashMap<String, AtomicInteger> countMap
= new ConcurrentHashMap<String, AtomicInteger> ();
public void add(String key){
AtomicInteger count = countMap.computeIfAbsent(key k->new AtomicInteger());
//自增
count.incrementAndGet();
}
二、字元串搜尋:
以下代碼的作用是搜尋“{”出現的位置,是否有性能更好的替代方法?
String str = ...;
int index = str.indexOf("{");
答案:考慮到隻搜尋單個 char,String 還提供了根據 char 來搜尋的 API。高效代碼如下:
int index = str.indexOf('{'};
類似的還有 String.replace(char,char)、StringBuilder.append(char),比如拼接一個 JSON,可以使用 append(char)方法:
StringBuilder sb = new StringBuilder();
sb.append('{')...append(':')...apend('}')
三、I/O輸出:
以下代碼輸出 HTML,從性能的角度考慮有沒有調整的地方?
try {
writer.write("<html>");
writer.write("<body>");
writer.write("<h1>"+topic.getTitle+"</h1>");
writer.write("</body>");
writer.write("</html>");
} catch (IOException e) {
throws e;
}
答案:可以考慮合并輸出,減小 write 方法的調用次數。Writer 類内部的一些操作可能會影 響性能,比如,如果 Writer 是一個 StringWriter 執行個體,那麼 write 方法内部還會有擴容操作。如果是一個 Servlet 的 Writer,則可能會檢測是否需要輸出到用戶端。合并調用有助于提高性能:
writer.write("<html><body>");
writer.write("<h1>"+topic.getTitle+"</h1>");
writer.write("</body></html>");
另一方面,可以考慮使用模闆技術,比如模闆引擎。編寫一個模闆:
<html>
<body>
<h1>${topic.title}</h1>
</body>
</html>
模闆使得輸出内容更容易維護。同時模闆引擎也非常高效,性能幾乎跟采用 StringBuilder拼接字元串一樣高。大部分模闆引擎都會合并靜态文本,但很多 Web 伺服器的 JSP 在反編譯的時候卻沒有合并靜态文本。
四、字元串拼接:
以下代碼需要把對象的位址清單拼接成一個字元串,如何提高性能?
public String buildProvince(List<Org> orgs){
StringBuilder sb = new StringBuilder();
for(Org org:orgs){
if(sb.length()!=0){
sb.append(",")
}
sb.append(org.getProvinceId())
}
return sb.toString();
}
答案:可以考慮預先指定一個 StringBuilder 的容量,如果 provinceId 的長度不超過 3 位,那麼可以估算出 StringBuilder 的容量。使用逗号分隔,可以将 append(",")改成 append(',')。最後,也可以預先将 provinceId 對應的 int 轉為字元串以避免 int 轉 String 的性能消耗。調整後的代碼如下:
//建立一個緩存
static int cacheSize = 1024;
static String[] caches = new String[cacheSize];
static {
for(int i=0;i<cacheSize;i++){
caches[i] = String.valueOf(i);
}
}
public String buildProvince(List<Org> orgs){
if(orgs.isEmpty()){
return "";
}
StringBuilder sb = new StringBuilder(orgs.size()*4);
for(Org org:orgs){
if(sb.getLength!=0){
sb.append(',')
}
sb.append(int2String(org.getProvinceId())
}
return sb.toString();
}
public static String int2String(int data) {
if (data < cacheSize) {
return caches[data];
} else {
return String.valueOf(data);
}
}
五、方法的入參和出參:
以下這段代碼是否有改善的地方?
public Map<String, Object> queryPathInfo(Map<String, Object> param) {
return Dao.selectOne(basepath + "selRedeemInfo", param);
}
類似的,有的方法使用 JsonObject 作為輸入參數或傳回值:
JsonObject funcName(Context req, JsonObject params)
答案:無論使用 Map,還是使用 JsonObject,作為入參或傳回值,都非常難以閱讀。除非再次閱讀方法的完整實作過程,才會知道 Map 或 JsonObject 到底包含什麼樣的資料。建議使用具體的對象代替這種了解較為抽象的類:
public PathInfo queryPathInfo(PathQuery param) {
//大部分 Dao 架構都支援映射資料庫結果集到一個個具體的類上
return Dao.selectOne(basepath + "selRedeemInfo", param,PathInfo.class);
}
六、RPC 調用定義的傳回值:
一個 RPC 架構的傳回值的定義如下,使用 Object 數組是否可行?
public Object[] rpcCall(Request request ){
try{
Object ret = proxy.query(request);
return new Object[]{true,ret};
}catch(Exception ex){
return new Object[]{false};
}
}
類似的,一個 JPA 的查詢結果如下:
@Query("select t.itemtype, count(t.id) from TxxItem t where t.parentCode like ?1
and t.state='1' group by t.itemtype ")
public List<Object[]> getFaciCount(String parentCode );
答案:使用對象,而不要使用數組。
public CallResult rpcCall(Request request){}
class CallResult{
boolean success;
Object ret;
}
JPA 的查詢結果傳回 List,這也使得維護者不得不閱讀 SQL 才知道傳回的值,而且對于數字類型,count 傳回的結果根據驅動或資料庫的不同而不同,有的傳回 BigDecimal,有的傳回BigInteger,這種寫法帶來了隐患。
七、Integer 的使用:
以下代碼判斷兩個整數是否相等,代碼是否有問題?
public boolean isEqual(Integer a,Integer b){
return a==b;
}
下面這段代碼的最終結果是 null 嗎?
Double a = 1D;
Double b = 2D;
Double c = null;
Double d = isSuccess() ? a*b : c;
答案:這裡涉及裝箱和拆箱,第一個例子使用“==”比較兩個對象是否是同一個,在數字-128 到 127 之間,因為這個範圍的裝箱都使用了緩存,是以可以這麼用,如果數字不在這個範圍内,則應該用 equals 進行比較。
應該改成如下代碼:
public boolean isEqual(Integer a,Integer b){
if(a==b){
return true;
}
if(a==null||b==null){
return false;
}
return a.equals(b);
}
在三元表達式中,如果 isSuccess()傳回 false,則代碼片段會抛出空指針。位元組碼如下:
INVOKESTATIC com/ibeetl/code/style/Test1.isSuccess ()Z
IFEQ L0
//比較,如果為 true
ALOAD 1 //變量 a
INVOKEVIRTUAL java/lang/Double.doubleValue ()D
ALOAD //變量 b
INVOKEVIRTUAL java/lang/Double.doubleValue ()D
//a*b
DMUL
GOTO L1
L0
//false:
ALOAD 3 //變量 c,拆箱導緻空指針
INVOKEVIRTUAL java/lang/Double.doubleValue ()D
L1
INVOKESTATIC java/lang/Double.valueOf (D)Ljava/lang/Double;
ASTORE 4
RETURN
可以看到,在三元表達式中,由于 a*b 有拆箱,是以變量 c 也會拆箱,導緻空指針。可以改成以下代碼:
Double d = null;
if(isSuccess()){
d = a*b;
}else{
d = c;
}
内容摘自《高性能Java系統權威指南》第八章
李家智 著
本書特點:
内容上,總結作者從事Java開發20年來在頭部IT企業的高并發系統經曆的真實案例,極具參考意義和可讀性。
對于程式員和架構師而言,Java 系統的性能優化是一個超正常的挑戰。這是因為 Java 語言和 Java 運作平台,以及 Java 生态的複雜性決定了 Java 系統的性能優化不再是簡單的更新配置或者簡單的 “空間換時間”的技術實作,這涉及 Java 的各種知識點。
本書從高性能、易維護、代碼增強以及在微服務系統中編寫Java代碼的角度來描述如何實作高性能Java系統,結合真實案例,讓讀者能夠快速上手實戰。
風格上,本書的風格偏實戰,讀者可以下載下傳書中的示例代碼并運作測試。讀者可以從任意一章開始閱讀,掌握性能優化知識為公司的系統所用。
本書适合:
中進階程式員和架構師;
以及有志從事基礎技術研發、開源工具研發的極客閱讀;
也可以作為 Java 筆試和面試的參考書。