日志服务需要提供的功能有:
可以从外部安全地开启和关闭日志服务;
可以供多个线程安全地记录日志消息;
在日志服务关闭后,可以把剩余未记录的消息写入日志文件;
public classLogService
{private final BlockingQueuemsgQueue; //阻塞的消息队列保存日志消息private finalPrintWrite writer; //写消息到日志文件private finalLoggerThread logThread; //写日志的线程private booleanisShutdown; //表示日志服务是否已经关闭publicLogService(String file) throws FileNotFoundException
{
logThread= newLogThread();
writer = new PrintWrite(file);
}public voidstart()
{
logThread.start(); //启动日志线程
Runtime.getRuntime().addShutdownHook(newThread() { //添加关闭钩子,确保在没有调用stop方法的情况下,日志文件最终仍然会关闭
stop();
});
}public voidstop()
{synchronized(this) //需要先加锁,再修改isShutdown的值
{if(!isShutdown)
{
isShutdown= true;
logThread.interrupt(); //中断日志线程
}
}
}public voidlog(String message)
{synchronized(this) //需要先加锁,再访问isShutdown的值
{if(!isShutdown) //若日志服务没有关闭,则将消息加入消息队列,这里是典型的先验条件,声明isShutdown为volatile并不能解决同步的问题
msgQueue.put(message);else
throw new IllegalStateException("Log Service is shutdown"); //若日志服务已经关闭,则抛出IllegalStateException
}
}private class LoggerThread extendsThread
{public voidrun()
{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并发编程实战》