2009年7月8日 星期三

執行緒同步化之時間怪獸

昨天終於把專題Server端的Database Connection Pool改好了。原本以為是Thread之間的Race condition,造成資料庫記錄取得之前,連線就被回收連線的Thread給close掉。後來才發現是自己沒把 java.sql.Connection的autoCommit設為false,造成Connection在還沒有被commit的狀態下就被放回 Connection Pool裡,時間一到就帶著沒送出的資料一起被回收。問題根本不在Thread的同步,而是我幾乎忘記有autoCommit這東西。
不過,問題改好之後,發現更大的問題:速度超慢。更離譜的是,使用單執行緒的方式執行,竟然還比有快取的多執行緒池快10倍以上。昨天晚上看著 Profiler的分析到凌晨4點,仍然找不出問題在哪,只看到執行緒wait的頻率很高。白天上課一直恍神,完全無法理解老師在上什麼,不過可能也因為恍神太多,讓我在走回宿舍的路上精神飽滿,聽著Claudio Abbado指揮Lucerne Festival Orchestra演出的Mahler交響曲第二號,靈機一動想到了一個可能性:「會不會有人拿著互斥鎖,做一些很耗時間的工作?」回到宿舍看code,果如所料!

試比較下列這兩段程式碼,你比較喜歡哪一種寫法?

Code 1.
synchronized(this){
if(queue.isEmpty()){
if(connectionCount.get()< maxConnectionCount){
conn = new
DBConnection(this,getRawConnection());

connectionCount.incrementAndGet();
}
}
else{...}
}
Code 2.
synchronized(this){
if(queue.isEmpty()){
if(connectionCount.get()< maxConnectionCount){
newConn = true;
connectionCount.incrementAndGet();
}
}
else{...}
}

if(newConn)
conn = new DBConnection(this,getRawConnection());
一般而言,多數人都會選Code 1.,因為兩段程式碼功能一樣,Code1卻顯得比較簡潔直觀。然而,Code1就是我一開始寫的,很花時間的程式碼。

Code2和Code1最大的差別在於Code 2把 conn = new DBConnection(this,getRawConnection()); 從同步區塊取出。這麼做會讓效能增加數倍的原因是getRawConnection()操作太花時間。如果放在同步區塊,代表一次只有1個執行緒能夠呼叫這個方法。這個方法一次得花10秒鐘的時間去和遠端資料庫連繫,如果一次只有1個執行緒呼叫,則20個執行緒至少得花200秒。Code2的getRawConnection()呼叫因為是在同步區塊之外,能夠讓多個執行緒同時取用,就沒有這種問題。
這次的教訓告訴我們,可以放在同步區塊的程式碼越少越好。至少,非必要的話,不要把怪獸放進去。

沒有留言:

張貼留言