当前位置: 首页 > 科技观察

双重检查锁定和延迟初始化

时间:2023-03-17 21:48:17 科技观察

双重检查锁定的由来在Java程序中,有时需要推迟一些高开销对象的初始化,只有在真正使用该对象时才对其进行初始化。这时候,就需要使用Lazyinitialization技术了。延迟初始化的正确实现需要一些技巧,否则容易出现问题,下面会一一介绍。方案一publicclassUnsafeLazyInit{privatestaticInstanceinstance;publicstaticInstancegetInstance(){if(instance==null){instance=newInstance();}returninstance;}}这种方法的错误是显而易见的。如果两个线程分别调用getInstance,由于对共享变量的访问不同步,很可能会出现以下两种情况:1、线程A和B都看到实例没有初始化,所以分别初始化。2.instance=newInstance操作重新排序。实际的执行过程可能是:先分配内存,然后赋值给instance,最后进行初始化。如果是这种情况,其他线程可能会读取尚未初始化的实例对象。方案二publicclassUnsafeLazyInit{privatestaticInstanceinstance;publicstaticsynchronizedInstancegetInstance(){if(instance==null){instance=newInstance();}returninstance;}}这种方法的问题很明显。每次读取一个实例,都需要进行同步,这可能会对Performance产生很大的影响。方案三方案三是双重检测加锁的错误实现,见代码:publicclassUnsafeLazyInit{privatestaticInstanceinstance;publicstaticInstancegetInstance(){if(instance==null){synchronized(UnsafeLazyInit.classes){if(instance==null){instance=newInstance();}}}returninstance;}}这个方案貌似解决了上面的问题两种解决方案,但也是有问题的。问题来源instance=newInstance();这条语句在实际执行中可能会拆分成3条语句,如下:memory=allocate();ctorInstance(memory);//2instance=memory;//3按照权重排序规则,最后两条语句没有数据依赖关系,因此它们可以重新排序。重排序后,意味着实例域被赋值后,指向的对象可能还没有初始化,而实例域是静态域,可以被其他线程读取,所以其他线程可以读取未初始化的对象。完成的实例字段。基于volatile的解决方案解决这个问题,只需要禁止statement2和statement3的重排序,就可以使用volatile来修改实例了。privatevolatilestaticInstance实例;因为Volatile语义会禁止编译器对volatile之前的操作重新排序写入volatile之后。基于类初始化的解决方案Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应,C到LC的映射由JVM实现。每个线程在读取一个类的信息时,如果该类还没有初始化,就尝试获取LC进行初始化,获取失败则等待其他线程释放LC。如果能获取到LC,则需要判断类的初始化状态,如果是位初始化,则需要进行初始化。如果正在初始化,则等待其他线程完成初始化,如果已经初始化,则直接使用该类型对象。publicclassInstanceFactory{privatestaticclassInstanceHolder{publicstaticInstance=newInstance();}publicstaticInstancegetInstance(){returnInstanceHolder.instance;//这里会导致实例类被初始化}}结论延迟初始化字段减少了初始化类或创建实例的开销,但增加了零访问被延迟便利领域的开销。大多数情况下,正常初始化优于惰性初始化。如果确实需要对实例字段使用线程安全的惰性初始化,请使用上面介绍的基于volatile的惰性初始化方案;如果你真的需要对静态字段使用线程安全的惰性初始化,请使用上面基于类初始化方案的惰性初始化。