程式碼方式配置Log4j並實現執行緒級日誌管理 第四部分
文章目錄
一 非同步輸出模式
目前尚剩餘兩個需求,一個是實現日誌的非同步輸出模式,一個是實現日誌同時按日期和檔案大小進行備份。
非同步輸出模式在第三部分說過了,在這部分單獨講,日誌的備份則放在第五部分結束。
這部分我只提供一個設計思路,重心還是放在程式碼執行效能上,總體的思路為:
- 維護一個日誌內容集合
- 開闢單獨執行緒對日誌內容集合進行輸出
這裡需要考慮一個事情,如何維護一個日誌內容的集合?
- 第一點必須要滿足FIFO(先進先出),儘量保障日誌的輸出順序;
- 第二點不能阻塞,因為一旦集合的讀寫阻塞,會使執行緒一直握著系統資源,而導致CPU資源佔用率高,整體處理效能的降低;
- 第三點必須保證執行緒安全,因為一旦開啟非同步輸出模式,所有的應用執行緒不再通過ThreadLogger物件來直接輸出日誌,而會將日誌內容重定向到日誌內容集合中,這是一個多執行緒併發的寫入動作,非同步處理執行緒則單獨的執行讀操作,所以執行緒安全問題必須考慮!
綜上,我考慮使用ConcurrentLinkedQueue,這是一個非阻塞且執行緒的安全的FIFO佇列,其API有興趣的讀者可查閱相關文件。
二 增加非同步輸出模式開關
那麼按照這個設計思路首先要對LogUtil進行微調,增加一個是否開啟非同步模式的成員,一旦該標誌位為True,則LogUtil.log()方法不再直接進行日誌輸出,轉而將日誌內容寫入ConcurrentLinkedQueue。
/**
* 1.實現程式碼方式配置Log4j <br>
* 2.實現執行緒級日誌物件管理 <br>
* 3.實現日誌的非同步輸出模式 <br>
* 4.實現按日誌檔案大小及日期進行檔案備份
*
* @author 胡楠
*
*/
public final class LogUtil
{
……
/**
* 日誌內容集合,其他應用執行緒寫入日誌,非同步處理執行緒則將日誌讀出並寫入檔案
*/
private static ConcurrentLinkedQueue<String> queueLogBuffer = new ConcurrentLinkedQueue<String>();
/**
* 非同步輸出模式開關,預設false
*/
private static boolean ASYNCHRONOUS = false;
/**
* 設定日誌輸出模式
*
* @param asych
*/
public static void setLogMode(boolean asych)
{
ASYNCHRONOUS = asych;
}
……
}
三 重構日誌輸出介面
既然已經加入非同步輸出模式,那麼LogUtil提供輸出介面則不能在單純的進行日誌輸出了,首先要判斷是否為非同步模式,如果是的話則需要將日誌內容寫入ConcurrentLinkedQueue:
private static void log(LogLevel level, String message)
{
if (ASYNCHRONOUS)
{
queueLogBuffer.offer(message);
return;
}
if (level == LogLevel.Trace)
{
getThreadLogger().logTrace(message);
}
else if (level == LogLevel.Debug)
{
……
}
四 非同步處理執行緒
非同步處理執行緒的邏輯要略嚴格些,我們必須要考慮當ConcurrentLinkedQueue中無內容的時候,需要讓執行緒釋放CPU資源,僅當其他應用執行緒放入內容的時候才將其喚醒,這時候我們需要使用“等待-通知”模式:
/**
* 非同步處理執行緒,使用等待通知模式,僅當其他應用執行緒放入內容,才喚醒該執行緒,否則執行緒放棄CPU資源
*
* @author 胡楠
*
*/
class LogBufferProcessor implements Runnable
{
public void run()
{
synchronized (queueLogBuffer)
{
while (queueLogBuffer.poll() != null)
{
// TODO 進行日誌輸出
}
try
{
queueLogBuffer.wait();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
如上是一個簡單的設計結構,如何進行日誌輸出,以及wait()中斷等處理都沒有很細緻的實現,還是那句話,這裡僅敘述設計思路,更加具體的實現,需要各位看官依自己的實際需求來實現。
注意,按如上方式進行處理,需要對log()方法再次重構,因為一旦應用執行緒向ConcurrentLinkedQueue中放入內容,則需要喚醒非同步處理執行緒,如下:
private static void log(LogLevel level, String message)
{
if (ASYNCHRONOUS)
{
queueLogBuffer.offer(message);
queueLogBuffer.notifyAll();
return;
}
……
五 總結及一些其他的建議
如果你是一名剛入門Java的朋友,那麼在接觸執行緒操作的時候,一定別忘了關注效能問題,正如上面的設計,如果我們不採用“等待-通知”模式,不論執行緒是否有任務需要處理,它都會一直持有CPU資源,這就拖累了系統的整體效能。
另外,我們還需要考慮一個事情,日誌內容佇列是否可以無限大,ConcurrentLinkedQueue是無界的,這意味著一旦應用執行緒瘋狂的進行日誌輸出,很可能出現記憶體溢位,所以我建議對ConcurrentLinkedQueue設定閾值,一旦超過閾值,則轉換為同步輸出模式,雖然降低了效能,但是迴避了記憶體溢位風險。
雖然設計了非同步輸出模式,我依然建議補充快取設計,即使非同步模式把日誌輸出重定向到了單獨的處理執行緒,但是頻繁的IO操作依然是系統性能的負擔,所以應該實現一個日誌快取,只有當快取內容達到快取閾值才進行一次IO操作,這樣會更大的提升整體效能。
請不要因為我貼上的程式碼不全而噴我不負責任,每個人每個專案的每個需求都有所差異,能夠從別人的設計思路中獲取自己需要的東西,舉一反三才是最寶貴的。
剩下最後第五部分,我會把自己重寫的Appender貼上來,它實現了同時按日期及日誌檔案大小進行日誌的備份,因為涉及對原始碼的延展,所以程式碼會貼的相對更全面一些,但是別抱太大期望,因為真的很簡單……(別瞎百度,我搜過,百度上好多人都是瞎寫的,各種複製貼上,只要看過原始碼,跟蹤過執行過程,你也能很容易實現定製化的需求)