一、 程式現狀
程式為基于Tomcat的WEB應用,在并發請求很少的情況下,程式運作正常,而當并發請求較多(70~300/30秒)時,WEB應用的頁面幾乎無法通路,通常需要重新整理多次才可能成功通路一次。
通過Tomcat的status頁面,可以發現Tomcat 目前線程數已達配置檔案中設定的最大值(800個https,200個http),并且目前所有線程均處于忙碌狀态,大部分線程的生存期比較長,最長的可達20分鐘。
觀察資料庫連接配接池,資料庫連接配接數量已達配置檔案設定最大值,但繁忙的資料庫連接配接并不多,大部分處于空閑狀态。
二、 調試與跟蹤面臨的問題
多線程程式相對于單線程程式在跟蹤和調試方面要麻煩許多,特别是在當衆多線程啟動時才會發生的BUG,期望通過IDE進行調試是不可能完成的任務。此時,記錄log是最容易想到的跟蹤方式。
但是在多線程中記錄log面臨的問題是由于線程衆多,記錄的log是也是由線程混雜生成的,因而很難從中抽取出一個線程的執行log進行分析。但是,要有效地分析線程運作情況,必須從繁雜的線程中抽取出一個線程的執行log。
顯然,如果每一條log能夠有目前線程(更确切地說,是針對目前請求的響應)的惟一辨別,那麼抽取一個線程就成為可能。
三、 日志記錄解決方案
要在log中辨別惟一線程,很容易想到的方式是在記錄log時同時記錄線程ID,但是對于Tomcat及類似的WEB的應用,使用線程ID存在以下兩個問題:
- Tomcat等伺服器會維護一個線程池,線程池中的線程會被反複使用,以便高效地響應請求,在這些種情況下,會有多個請求的log使用同一辨別,依然無法抽取針對一次請求響應的log。
- JDK1.4及之前并不支援線程ID,而目前面臨的應用正好使用的是JDK1.4.2。
利用線程類Thread提供的以下方法,可以實作對對每一請求進行惟一辨別:
public static Thread currentThread() // 取得目前線程對象
public final void setName(String name) // 為線程設定一個名稱
public final String getName() // 取得線程的名稱
具體實作過程如下:
- 在請求的入口處通過currentThread方法取得線程對象,然後調用其setName方法為線程設定一個惟一辨別,惟一辨別根據不同的應用可以設計不同的辨別,隻要其符合惟一性的标準即可。
- 在需要添加log的地方通過通過currentThread方法取得線程對象,然後調用getName方法取得目前線程的名稱,并将其記入目前log。
- 由于懷疑是某些操作耗時過長,是以在需要記錄log的方法中,入口處與出口處均需添加log。
- 更新程式後,取得log檔案即可抽取一個請求的執行流程進行分析。
四、 日志結果分析
通過日志,很快發現線上程中,以下方法被頻繁調用并且耗時較長:
synchronized public static XXX getInstance()
{
if (m_instance == null)
{
m_instance = new XXX();
}
return m_instance;
}
該類是一個單執行個體類,通過分析其代碼,并沒有需要特别注意線程安全的地方,故可以直接取消方法getInstance的同步特性,隻是這樣可能線上程調用getInstance産生多餘一個的執行個體,但此後它會被Java進行垃圾回收,并沒有太大影響。或者,可以将同步範圍縮小至需要建立對象的代碼塊處,這樣将僅在程式剛開始運作時受到互斥的影響。
public static XXX getInstance()
{
if (m_instance == null)
{
Synchronized(this)
{
if (m_instance == null)
{
m_instance = new XXX();
}
}
}
return m_instance;
}
五、 總結
- 多線程應用,取得能夠惟一辨別一個線程的log是迅速查找問題的關鍵,單純依據現象及經驗進行分析,可能會耗費大量的時間。前期在分析問題可能産生原因的同時,能取得有效log是一項優先度極高的工作。
- 單實列類的使用要慎重,其對空間占用率的影響可能遠不如因同步而帶來的負面影響。通常,應當隻将具有狀态的類設計成單執行個體類。
- 看起來似乎可以很快執行完畢的同步方法,線上程衆多、被頻繁調用時,也有可能是線程執行的瓶頸所在,不要将關注點隻放在哪些可能比較耗時的操作上。
歡迎通路夢斷酒醒的部落格