最近常在代码中见到延迟初始化,想和大家聊聊这个小话题。最简单的延迟初始化很容易想,提起键盘我就能敲出来啊:
public class DelayInitializationWithSingleThread {
private static Object foo;
public static Object getInstance() {
if(foo == null)
this.foo = new Object();
return this.foo;
}
}
这个延迟初始化,单线程下很完美,多线程下岂不是要乱套!那简单,那我在方法上加一个同步操作好了:
public class DelayInitializationWithMultiThread {
private static Object foo;
public synchronized static Object getInstance() {
if(foo == null)
this.foo = new Object();
return this.foo;
}
}
但是加锁需要上下文切换,是个很耗系统资源的操作,每次调用getInstance()都要执行一次上下文切换,有些没必要。我只需要在第一次初始化的时候进行加锁就好了啊,这就是著名的DCL(Double Check Lock):
public class DelayInitializationWithDCL {
private static Object foo;
public synchronized static Object getInstance() {
if(foo == null) {
synchronized (Test.class) {
if(foo == null)
this.foo = new Object();
}
}
return this.foo;
}
}
看起来很不错。
等一下,还记得JVM的可见性吗?每个线程的变量都是在各自线程的缓存中,各自的工作变量相互不可见,必须等到JVM将其刷到公用的主存里才能对其他线程可见。也就是说A线程初始化foo后,B线程不一定能看得到,在不知道的情况下,那B会再执行一遍初始化,C线程也不一定看得见,他继续。。。这还只是个简单的示例代码,要是工作代码执行一些复杂的初始化的话,那不是可能造成对象失效或者脏数据?好吧我不敢再往下想了,赶紧给变量foo加上消除线程缓存作用的volatile
关键字。
public class DelayInitializationWithVolatiledDCL {
private volatile static Object foo;
public synchronized static Object getInstance() {
if(foo == null) {
synchronized (Test.class) {
if(foo == null)
this.foo = new Object();
}
}
return this.foo;
}
}
呼呼。。这下终于完成了,看着很完美。但是啊,有些复杂,万一哪天我头脑发热,忘记加volatile
咋办呢。好吧,为了有更好的方案,继续改造:
public class DelayInitializationWithInitalizationPlaceHolder {
private static class ResourceHolder {
public static Object foo = new Object();
}
public synchronized Object getInstance() {
return ResourceHolder.foo;
}
}
当getInstance()第一次被调用时,JVM会加载ResourceHolder类,并将其静态变量初始化,以后再次调用就能直接获取到初始化的foo了,不需要加锁,不需要volatile,简直太棒了,完美!