一 异步输出模式
目前尚剩余两个需求,一个是实现日志的异步输出模式,一个是实现日志同时按日期和文件大小进行备份。
异步输出模式在第三部分说过了,在这部分单独讲,日志的备份则放在第五部分结束。
这部分我只提供一个设计思路,重心还是放在代码执行性能上,总体的思路为:
- 维护一个日志内容集合
- 开辟单独线程对日志内容集合进行输出
这里需要考虑一个事情,如何维护一个日志内容的集合?
- 第一点必须要满足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贴上来,它实现了同时按日期及日志文件大小进行日志的备份,因为涉及对源码的延展,所以代码会贴的相对更全面一些,但是别抱太大期望,因为真的很简单……(别瞎百度,我搜过,百度上好多人都是瞎写的,各种复制粘贴,只要看过源码,跟踪过执行过程,你也能很容易实现定制化的需求)