Bug来源:
这个问题是在实现storm慢请求报警功能时,MailCache类引用了Environments的静态方法。
// class MailCache
private static final UrlMap DEFAULT_URLMAP = Environments.getDefaultUrlMap();
在Environments类中,我想在类被初始化时就开一个定时更新cache的定时器,就把它放在了static初始化块中,而updateCache引用了MailCache,因此,MailCache构造函数执行时,它的静态初始化过程还未完成。
public static Map<String, Integer> mRegex2Threshold;
public static ScheduledExecutorService mScheduExec;
static {
updateCache();//updateCache会执行MailCache的静态方法
mScheduExec.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
updateCache();
}
}, 15, 15, TimeUnit.SECONDS);
}
Bug简化描述:
在多个类的加载过程中,类加载的顺序是不确定的。当类A引用B时,如果B类还未被加载,则会在本线程中暂停A的执行,去加载B,然后继续执行A,称这个过程是Load_B
类被加载后,一个类或实例的初始化过程是:
step 1、静态变量(初始化块)
step 2、非静态变量(初始化块)
step 3、构造函数
step 2和 step 3的发生必须有new关键字的触发。
如果Load_B发生在A类的step1时,则会造成A的初始化过程被打断,会一起一些问题,比如,A的构造函数会在A静态变量未初始化完成之前被执行!
如下例:
public class B {
static {
System.out.println("B init");
}
static int hook = A.hook;
static int bug = 3;
static {
System.out.println("B other");
}
public B() {
System.out.println("b bug=" + bug);
}
public static void main(String... args) {
}
}
public class A {
static {
System.out.println("A init");
}
static int hook;
static B b = new B();
static {
System.out.println("A other");
}
}
当初始化到class A的b变量时,执行class B的构造函数,而此时class B的静态变量还未初始化完毕,也就是构造函数被提前执行了。 由于main方法的存在,class B先被加载,然后执行step 1。当初始化到 hook变量时,发现class A未加载,所以转而加载class A,然后执行A的step 1。
以上程序运行输出:
B init
A init
b bug=0
A other
B other
可以看出,class B构造函数执行时,bug变量的值并不是3,而是int类型的默认值0。
当A和B的逻辑更复杂时,这样的Bug难以定位。造成该Bug的本质原因是A和B存在相互引用的static变量,有以下解决方法:
1、把引用代码,如hook=A.hook放在最下面,即让它成为静态初始化的最后一步。但这样做,代码难以维护,维护者可能会莫名其妙地调到坑里。
2、把涉及相互引用的代码变成非static变量,然后用单例模式保证变量的唯一,改造后的代码:
public class A {
static {
System.out.println("A init");
}
static int hook;
B b = new B();
static {
System.out.println("A other");
}
static A _holder = null;
private A() {
}
public static A getInstance() {
if (_holder == null) {
synchronized (A.class) {
if (_holder == null) {
_holder = new A();
}
}
}
return _holder;
}
}