第八十三条:谨慎使用延迟初始化
延迟初始化是将字段的初始化延迟到需要它的值时。如果不需要该值,则不会初始化该字段。这种技术既适用于静态字段,也适用于实例字段。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环 [Bloch05, Puzzle 51]。
与大多数优化一样,对于延迟初始化,最好的建议是 “除非需要,否则不要做” (item 67)。延迟初始化是一把双刃剑。它减少了初始化类或创建实例的成本,但增加了访问延迟初始化字段的成本。
延迟初始化(像许多“优化”一样)实际上会损害性能,具体取决于这些字段中最终需要初始化的部分、初始化的开销以及初始化后每个字段被访问的频率。
也就是说,延迟初始化有它的用处。如果字段只在类实例的一部分上访问,并且初始化字段的开销很大,那么延迟初始化可能是值得的。确定的唯一方法是在延迟初始化和不延迟初始化的情况下度量类的性能。
在存在多个线程的情况下,延迟初始化是很棘手的。如果两个或多个线程共享一个延迟初始化的字段,就必须使用某种形式的同步,否则可能导致严重的错误(item 78)。本项目中讨论的所有初始化技术都是线程安全的。
在大多数情况下,正常初始化比延迟初始化更可取。下面是一个通常初始化的实例字段的典型声明。注意 final 的使用(item 17):
//正常初始化一个实例
private final FieldType field = computeFieldValue();
如果你使用延迟初始化来打破初始化循环,使用 synchronized ,因为它是最简单,最清晰的替代:
// 实例字段的延迟初始化---使用了同步访问器的方法
private FieldType field;
private synchronized FieldType getField() {
if (field == null)
field = computeFieldValue();
return field;
}
这两种习惯用法(使用 synchronized 的常规初始化和延迟初始化) 在应用于静态字段时没有变化,只是在字段和访问器声明中添加了静态修饰符。
如果需要在静态字段上使用延迟初始化以提高性能,请将延迟初始化的资源放在一个 holder 类中管理。这个技术利用了类在被使用之前不会被初始化的保证 [JLS, 12.4.1]。下面是它的样子:
// 用于静态字段的延迟初始化holder类习惯用法
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
当 getField 第一次被调用时,它读取 FieldHolder.field,导致初始化 FieldHolder 类。这种习惯用法的美妙之处在于,getField 方法没有同步,并且只执行字段访问,因此延迟初始化实际上不会增加访问成本。典型的虚拟机只会为了初始化类而同步字段访问。类初始化后,虚拟机会自动初始化静态成员,后续对字段的访问不需要涉及任何测试或同步。
如果需要在实例字段上使用延迟初始化以提高性能,请使用双重检查习惯用法。这个习惯用法避免了初始化后访问字段时的锁定开销(item 79)。这种习惯用法背后的思想是检查字段的值两次(因此得名 double-check):第一次不加锁定,然后,如果字段看起来没有初始化,第二次使用锁定。只有在第二个检查表明该字段未初始化时,调用才初始化该字段。因为字段初始化后就没有锁定,所以将该字段声明为 volatile 非常关键(item 78) :
//用于延迟初始化实例字段的双重检查习惯用法
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null) { // 第一次检查 (不锁定)
synchronized(this) {
if (field == null) // 第二次检查 (锁定)
field = result = computeFieldValue();
}
}
return result;
}
这段代码可能看起来有点复杂。特别是,对局部变量 (result) 的需求可能不清楚。这个变量的作用是确保字段在已经初始化的情况下只被读取一次。虽然不是绝对必要的,但这可以提高性能,而且按照应用于低级并发编程的标准来看,这更优雅。在我的机器上,上面的方法比没有局部变量的版本快 1.4 倍。
虽然您也可以对静态字段应用双重检查习惯用法,但是没有理由这样做:延迟初始化holder类是更好的选择。
需要注意双重检查习语的两个变体。有时,您可能需要延迟初始化一个可以容忍重复初始化的实例字段。如果您遇到这种情况,您可以使用双重检查习语的变体来避免第二次检查。毫不奇怪,它被称为单检查习语。这是它的样子。注意,字段仍然声明为volatile:
// 单次检查习惯用法 - 可能会引发重复初始化
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
本项目中讨论的所有初始化技术都适用于原始类型和对象。当对数值基元字段应用双重检查或单次检查时,将根据0 (数值基元变量的默认值) 而不是 null 检查字段的值。
如果您不关心是否每个线程都重新计算字段的值,并且字段的类型不是 long 或double,那么您可以选择在单检查习惯用法中从字段声明中删除 volatile 修饰符。这种变体被称为活泼的单勾选习语。在某些体系结构上,它加速了字段访问,但代价是额外的初始化(每个访问字段的线程最多进行一次初始化)。这绝对是一种新奇的技术,不适合日常使用。
总之,您应该正常地初始化大多数字段,而不是延迟地初始化。如果为了实现性能目标或打破有害的初始化循环,必须延迟初始化字段,那么请使用适当的延迟初始化技术。例如字段,它是双重检查习语;对于静态字段,延迟初始化holder类。对于允许重复初始化的实例字段,还可以考虑单次检查习惯用法。
所有文章无条件开放,顺手点个赞不为过吧!
原文地址:https://blog.csdn.net/weixin_56542271/article/details/144275740
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!