日志服務需要提供的功能有:
可以從外部安全地開啟和關閉日志服務;
可以供多個線程安全地記錄日志消息;
在日志服務關閉后,可以把剩余未記錄的消息寫入日志文件;
public class LogService { private final BlockingQueue<String> msgQueue; //阻塞的消息隊列保存日志消息 private final PrintWrite writer; //寫消息到日志文件 private final LoggerThread logThread; //寫日志的線程 private boolean isShutdown; //表示日志服務是否已經關閉 public LogService(String file) throws FileNotFoundException { logThread = new LogThread();
writer = new PrintWrite(file); } public void start() { logThread.start(); //啟動日志線程 Runtime.getRuntime().addShutdownHook(new Thread() { //添加關閉鈎子,確保在沒有調用stop方法的情況下,日志文件最終仍然會關閉 stop(); }); } public void stop() { synchronized(this) //需要先加鎖,再修改isShutdown的值 { if(!isShutdown) { isShutdown = true; logThread.interrupt(); //中斷日志線程 } } } public void log(String message) { synchronized(this) //需要先加鎖,再訪問isShutdown的值 { if(!isShutdown) //若日志服務沒有關閉,則將消息加入消息隊列,這里是典型的先驗條件,聲明isShutdown為volatile並不能解決同步的問題 msgQueue.put(message); else throw new IllegalStateException("Log Service is shutdown"); //若日志服務已經關閉,則拋出IllegalStateException } } private class LoggerThread extends Thread { public void run() { try { while(true) { try { synchronized(LogService.this) { if(isShutdown && msgQueue.size() == 0) //如果服務已經關閉並且消息隊列中已經沒有剩余的消息,則關閉日志線程 break; writer.write(msgQueue.take()); } } catch(InterruptedException ex){} //忽略中斷消息 } } finally { writer.close(); //關閉日志文件 } } } }
在上面的例子中,有以下幾個地方值得注意:
日志服務不應該在收到關閉消息時立即停止,而應該將消息隊列中剩余的消息寫入到日志文件之后再關閉。如果決定丟棄這些消息,那么應該先清空消息隊列,否則調用log方法的線程會一直阻塞;
上例中使用isShutdown來標識服務是否已經關閉,調用log方法的線程首先檢測isShutdown的值,這樣多個線程就需要對isShutdown互斥訪問,而不能簡單使用volatile修飾isShutdown;
在日志線程中,檢測到中斷消息后,直接忽略了,最后在finally中也沒有再恢復中斷狀態,這是因為我們知道線程的所有者日志服務已經停止了,不再需要恢復中斷;
上例中使用了關閉鈎子,在start方法中添加了關閉鈎子線程,可以確保即使調用者沒有調用stop方法停止日志服務,日志服務最終在JVM停止之前也會關閉;
下面簡單介紹一下關閉鈎子:
關閉鈎子是通過Runtime.addShutdown方法注冊的但並不立刻啟動任務的線程,JVM在關閉過程中,首先會啟動執行已經注冊的關閉鈎子線程。關閉鈎子通常用於實現服務或者應用程序的清理工作,並且不宜在其中執行耗時的任務,會延遲JVM關閉的時間。
參考資料 《Java並發編程實戰》