lazy-init 懒加载的艺术
懒加载是一种加载方式,加载单例对象一般有两种方式,一是在启动时就立即创建好,另一种则是在需要用到的时候再去加载即懒加载。懒加载一般会针对单例场景,且一般是针对在加载消耗较大费时,且不一定会用到的场景。
好了,相信啥意思大家都明白!那么具体如何实现呢?其实挺有意思的!
方案1. 直接用懒加载实例进行判断null,不安全的做法
public class UnsafeLazyInit{ private static Instance instance; public static Instance getInstance(){ if (instance == null){ instance = new Instance(); } return instance; } }
很明显,这里的懒加载是不是安全的,因为当线程并发访问 getInstance(), 可能同时看到为null,从而同时进行了初始化,这会导致外部获取到的实例是不致的,从而导致不可预估的错误!另一种隐形的加载错误待说!
方案2. 使用锁将懒加载方法锁起来,不省力的做法
如上,既然既然访问 getInstance() 是非线程安全的,那么,只要加个锁就可以了!
public static syncronized Instance getInstance(){ if (instance == null){ instance = new Instance(); } return instance; }
很明显,这种做法在高并发的情况下,会有严重的锁竞争,从而导致严重的性能问题!相信不会有同学这么干!
方案3. 双重锁校验的普通方法,一个有隐患的做法
如上两个问题,既要线程安全,又要性能不影响,其实可以想像到,初始化动作只是一次性的,所以,只要第一次的时候保证线程安全即可,因为后续大家都是获取同一个实例!所以,我们把锁的位置放到第一次加载时!
public static Instance getInstance(){ if (instance == null){ // 只有在instance为null即未进行过初始化时,才会上锁,从而避免后续性能问题 synchronized(UnsafeLazyInit.class){ // 由于外面的判定是非线程安全的,上锁后,再次进行判定是否已创建 if (instance == null){ // 此处初始化可能出现重排序 instance = new Instance(); } } } return instance; }
所以,如上的解决办法,看起来很完美。但其实还是有问题的!问题在于,外部的 instance == null 初始是非线程安全的,任何进入的线程都可以进行断定!
而 instance = new Instance(); 语句,并不是像代码看起来那样,就一句,可以保证原子性!
这一条语句在实际执行中,可能会被拆分程三条语句,即分配内存/初始化类变量/赋值给实例变量,大致如下:
memory = allocate();
ctorInstance(memory);
instance = memory;
由于jvm的jit编译优化,可能会重排序,在保证结果最终一致的前提下,会将分配内存和赋值实例变量做不确定的重排,而当发生重排后,即先赋值实例变量内存空间,那么由于外部非线程安全的获取实例变量,会立即读取到该变量不为null,从而得到一个未初始化完成的实例进行后续操作!这将带来不可预知的后果!所以,这种双重锁是有问题的!不过看起来问题范围已经很小了!
方案4. 双重锁校验的增强方法,完美
综上双重锁方法,还存在一个重排序问题,一般针对重排序,我们要条件反射式的想到禁止重排序即可。而jvm禁止重排的方式,有 volatile, final, 等关键词,当然其实现原理都是加入一些内存屏障来保障不重排。不管怎么样,我们只需要使用一些关键词就可以了!
private volatile static Instance instance; public static Instance getInstance(){ if (instance == null){ // 只有在instance为null即未进行过初始化时,才会上锁,从而避免后续性能问题 synchronized(UnsafeLazyInit.class){ // 由于外面的判定是非线程安全的,上锁后,再次进行判定是否已创建 if (instance == null){ // 使用volatile后,禁止了jit重排优化 instance = new Instance(); } } } return instance; }
方案5. 使用类初始化机制创建对象,一种脆弱的加载方式
这是一种基于类初始化锁的一种懒加载方式!将懒加载放在一个类的静态变量上,依赖于类的安全的类加载来保证期实例化的线程安全性和准确性!如下:
public class InstanceFactory { private static class InstanceHolder { // 懒加载实例化放到内部类的静态变量上,需确定两个问题,1. 初始化时机,2. 线程安全性 public static Instance instance = new Instance(); } public static Instance getInstance() { return InstanceHolder.instance; } }
这看起来虽然有点麻烦,但是理解起来不会有问题!但是有问题我们得考虑下:静态变量不是一开始就会加载出来吗?如果这样的话,就不存在懒加载了啊!
其实static静态变量是在类初始化的时候才会操作的。
而类的初始化则有几个时机:
1. 类首次被创建实例,即 new xxx() 操作时,触发类初始化; 2. 类中的静态方法被首次调用,比如上面的 getInstance() 被首次调用时会触发当前类的初始化; 3. 类的静态字段被赋值,比如 A.instance = abc; 4. 类中的一个非常量字段被使用,常量则不会触发初始化; 5. 类一个顶级类,而且一个断言语句嵌套在类内部执行;(我也不太明白啥意思)
来看一下实际的例子,说明类的初始化时机:
@Test public void testClassInit() { // new 就不多说了 // 静态方法被使用 A.getInstance(); // 常量使用不会触发类初始化 System.out.println(A.noneInitConst); // 静态变量被赋值触发类初始化 A.setVarInit = ""; System.out.println("setVarInit=''"); // 非常量静态变量被使用触发类初始化 System.out.println(A.usedVarInit); } static class A { // 类常量被使用,不会触发类初始化 public static final String noneInitConst = "a"; // 静态变量赋值 static String setVarInit; // 静态变量使用,触发类初始化 static String usedVarInit = "c"; static { // 类初始化时会执行该静态块 System.out.println("A executed..."); } // 静态方法触发类初始化 public static String getInstance() { // 为避免其他规则被触发,直接使用返回字符串 return "A"; } }
可以依次注释各规则,查看初始化效果!
ok,明白了类初始化的时机后,我们知道了,这里的懒加载是有用的!
但是还有个问题,就是类初始化难道不会并发吗?答案是一定的,既然执行时机一致,那么并发自然存在。
类初始化时,jvm会去获取一个锁,从而保证同步多个线程对同一个类的初始化!这个从jdk的ClassLoader实现可以看出来!
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 获取锁 synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
所以,类的初始化是线程安全的!从而得出,使用类的 static 变量进行懒加载的正确性!
当然,这里无故又多了一个内部类,着实让人不爽,而且如果后面往这个类里加入其他变量,则可能一不小心导致了问题!
不管怎么样,他能解决当下的问题!
方案6. 使用第三方变量来标识懒加载情况
意思是说,这里的初始化,是一个大对象的初始化,那么我可以换一个 true | false, 的简单变量的判定来处理,变量虽简单,不过也可能遇到的问题其实和上面一样,就不赘述了! 具体做法就是,在完成加载之后,再将该第变量值改变即可!
但是使用第三方变量还有个好处,就是可以很方便的执行代码块!复杂的加载逻辑,你懂的!
综上,懒加载这个简单操作,还真是充满了艺术感呢!
参考: 《java并发编程的艺术》