原文链接见: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在这里是可以通过不相等判断。。