先回答主要问题:到底是io瓶颈,还是锁竞争导致线程block?
死锁是显而易见的,一是因为它会一直阻塞而系统load很低,二是也很容易通过jstack监测到死锁。
另外一个问题:另外我对他文末提供的关于每个调用处static 一个Logger的做法并不能解决锁竞争的问题
static确实无法解决问题,后面的复现例子会演示这一点。
Bugzilla上的这个评论已经提出了一种死锁的场景。其实Category#callAppenders里面的同步块本身并没有大问题,甚至AppenderAttachableImpl#appendLoopOnAppenders方法里还会调用一个synchronized修饰的AppenderSkeleton#doAppend方法,但是两层加锁是有一致顺序的,不会出现死锁。
真正的问题在于,你传递给Logger#info(Object message)系列方法的这个message对象,鬼知道它的Object#toString方法是怎么实现的。
解决方案见最后,我先简单复现一下:
log4j_dead.properties
log4j.rootLogger = INFO,CONSOLE
log4j.logger.CHILD=INFO,CONSOLE
log4j.additivity.CHILD=false
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.SimpleLayout
Log4jDeadLockTest.java
package com.demo;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.junit.Test;
/*** log4j 1.2.17*/
public class Log4jDeadLockTest {
static {
System.setProperty("log4j.configuration", "log4j_dead.properties");
}
static class Lazy {
// static的解法其实没啥用private static final Logger CHILD = Logger.getLogger("CHILD");
private static final Logger ROOT = Logger.getRootLogger();
}
@Test
public void test() {
Lazy.CHILD.info(new Object() {
@Override
public String toString() {
// 1. main线程:CHILD logger锁和CONSOLE appender锁已获得//
new Thread(new Runnable() {
@Override
public void run() {
// 2. background线程:ROOT logger锁已获得,阻塞等待CONSOLE appender锁Lazy.ROOT.info(null);
}
}, "background").start();
try {
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
// 3. main线程:阻塞等待ROOT logger锁Lazy.ROOT.info(null);
return null;
}
});
}
}
有啥简单的解决方法呢?我在编写上面的demo的过程中,第一个版本习惯性用的slf4j+log4j组合,结果无法复现,然后我翻了下源码:
org.slf4j.impl.Log4jLoggerAdapter.java
public void info(String format, Object... argArray) {
if (logger.isInfoEnabled()) {
FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
logger.log(FQCN, Level.INFO, ft.getMessage(), ft.getThrowable());
}
}