原文链接见:http://sw1982.iteye.com/admin/blogs/611545
在上文中提到扩展appener方法的实现,结果本周同事在一段代码里面出现了log重复的情况。
情况描述是:root log文件输出一次,被我扩展过的分模块appender却根据Executor线程数重复输出n次。经过一番debug(多线程debug真的比较恶心...经验是配合一些sysout查看临界,否则线程被刮起很难模拟并发)
test代码如下:(这个testcase可以借鉴的地方是,在线程里面起executor,可以比较方便的mock多线程并发)
package test; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import com.paipai.core.logging.Logger; import com.paipai.logicframework.biz.service.BaseService; import com.paipai.logicframework.common.logging.LoggerUtil; /** * created on 2010-5-6下午02:22:42 * * @author weisong */ public class MutilThreadLog { public static void main(String[] args) { Work work = new Work(); work.doA(); } } class Work { private ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors .newFixedThreadPool(3); private static Logger logger = LoggerUtil.getLogger(); public void doA() { String[] sss = { "aa", "bb", "cc", "dd","ee","ff","gg","hh" ,"ii","jj","kk","ll","mm","nn","oo","pp","qq","xx","yy","zz"}; for (final String s : sss) { Thread thread = new Thread() { @Override public void run() { logger.log("descnotfound", s); } }; executor.execute(thread); } } }
问题分析:(同步块过小)
org.apache.log4j.Logger logger = Log4jFactory.getInstance(logName); if (logger.getAppender(logName) == null) { initBusiLogAppender(logName, busiModule+".log", logger); }
这段代码,将logger对象的获取变成工厂模式,确保了单例。 getAppender函数通过源码可以看到是synchronized。
but,但是,initBusiLogAppender这个函数却是线程不安全的!(具体代码请参看本文开头的博文链接)
出现线程不安全的步骤为:
1.当前class实例获取到 logger对象, 这个时候没有"logName"这个appender
2.当前class实例持有一个Executor,启动了多个线程, 每个线程都会去持有同一个logger对象。(多线程访问本地变量,应该是final,或者voliate的。当然这里的logger对象本身就会是final)
3.线程并发,每个线程都会通过if判断条件,进入initBusiLogAppender方法。(虽然getAppender()是一个同步方法,
可是锁的范围并未涉及到if条件内部)
4. initBusiLogAppender重复初始化n次。。然后就悲剧了。
解决方案:
private void initBusiLogAppender(String appenderName, String logFileName, org.apache.log4j.Logger log) { synchronized(log) { if(log.getAppender(appenderName)!=null) return; try {
因为走到initBusiLogAppender函数的机会并不多,所以在这个对象上直接做同步。
其他的废话:
初步看起来log4j也是有点小问题的,比如定义了如下方法:
/** Look for the appender named as <code>name</code>. <p>Return the appender with that name if in the list. Return <code>null</code> otherwise. */ synchronized public Appender getAppender(String name) { if(aai == null || name == null) return null; return aai.getAppender(name); }
按说这么定义之后,一个log对象应该是不允许有name相同的appender。
但是本文所fix的那个bug,确实是因为初始化了多个相同appender导致!
跟进aai.getAppender(name)方法:这下好了,居然是遍历list取第一个name相同的返回,而不管是否存在相同name的appender。
public Appender getAppender(String name) { if(appenderList == null || name == null) return null; int size = appenderList.size(); Appender appender; for(int i = 0; i < size; i++) { appender = (Appender) appenderList.elementAt(i); if(name.equals(appender.getName())) return appender; } return null; }
那么继续跟进addAppender方法,看这里有无保证appender的名字不重复。
/** Attach an appender. If the appender is already in the list in won't be added again. */ public void addAppender(Appender newAppender) { // Null values for newAppender parameter are strictly forbidden. if(newAppender == null) return; if(appenderList == null) { appenderList = new Vector(1); } if(!appenderList.contains(newAppender)) appenderList.addElement(newAppender); }
这里使用appenderList.contains(newAppender),vecotr的对象比较来判断appender是否重复,但是不幸的是,
大多数Appender接口实现没有重载Ojbect的hashcode()方法,也就是name相同的appender在这里是可以通过不相等判断。。