1. 程式人生 > >程式碼方式配置Log4j並實現執行緒級日誌管理 第四部分

程式碼方式配置Log4j並實現執行緒級日誌管理 第四部分

文章目錄

一 非同步輸出模式

  目前尚剩餘兩個需求,一個是實現日誌的非同步輸出模式,一個是實現日誌同時按日期和檔案大小進行備份。

  非同步輸出模式在第三部分說過了,在這部分單獨講,日誌的備份則放在第五部分結束。

  這部分我只提供一個設計思路,重心還是放在程式碼執行效能上,總體的思路為:

  1. 維護一個日誌內容集合
  2. 開闢單獨執行緒對日誌內容集合進行輸出

  這裡需要考慮一個事情,如何維護一個日誌內容的集合?

  1. 第一點必須要滿足FIFO(先進先出),儘量保障日誌的輸出順序;
  2. 第二點不能阻塞,因為一旦集合的讀寫阻塞,會使執行緒一直握著系統資源,而導致CPU資源佔用率高,整體處理效能的降低;
  3. 第三點必須保證執行緒安全,因為一旦開啟非同步輸出模式,所有的應用執行緒不再通過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貼上來,它實現了同時按日期及日誌檔案大小進行日誌的備份,因為涉及對原始碼的延展,所以程式碼會貼的相對更全面一些,但是別抱太大期望,因為真的很簡單……(別瞎百度,我搜過,百度上好多人都是瞎寫的,各種複製貼上,只要看過原始碼,跟蹤過執行過程,你也能很容易實現定製化的需求)