日志服务需要提供的功能有:
可以从外部安全地开启和关闭日志服务;
可以供多个线程安全地记录日志消息;
在日志服务关闭后,可以把剩余未记录的消息写入日志文件;
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并发编程实战》