Effective Java 第三版——83. 明智谨慎地使用延迟初始化

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java

Effective Java, Third Edition

83. 明智谨慎地使用延迟初始化

延迟初始化(Lazy initialization)是延迟属性初始化直到须要其值的行为。 若是不须要该值,则永远不会初始化该属性。 此技术适用于静态和实例属性。 虽然延迟初始化主要是一种优化,但它也能够用来打破类和实例初始化中的有害循环[Bloch05,Puzzle 51]。git

与大多数优化同样,延迟初始化的最佳建议是“除非须要,不然不要这样作”(条目 67)。延迟初始化是一把双刃剑。它下降了初始化类或建立实例的成本,代价是增长了访问延迟初始化属性的成本。根据这些属性中最终须要初始化的部分、初始化它们的开销以及初始化后访问每一个属性的频率,延迟初始化实际上会下降性能(就像许多“优化”同样)。github

也就是说,延迟初始化有其用途。 若是仅在类的一小部分实例上访问属性,而且初始化属性的成本很高,则延迟初始化多是值得的。 确切知道的惟一方法是使用和不使用延迟初始化来测量类的性能。编程

在存在多个线程的状况下,延迟初始化很棘手。若是两个或多个线程共享一个延迟初始化的属性,那么必须使用某种形式的同步,不然会致使严重的错误(条目 78)。本条目中讨论的全部初始化技术都是线程安全的。安全

在大多数状况下,正常初始化优于延迟初始化。 如下是一般初始化的实例属性的典型声明。 注意使用final修饰符(条目 17):并发

// Normal initialization of an instance field
private final FieldType field = computeFieldValue();

若是使用延迟初始化来破坏初始化循环,请使用同步访问器,由于它是最简单,最清晰的替代方法:性能

// Lazy initialization of instance field - synchronized accessor
private FieldType field;

private synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}

当应用于静态属性时,这两个习惯用法(正常初始化和使用同步访问器的延迟初始化)都不会更改,除了将static修饰符添加到属性和访问器声明。测试

若是须要在静态属性上使用延迟初始化来提升性能,请使用延迟初始化持有者类(lazy initialization holder class)的习惯用法。这个习惯用法保证了一个类知道被使用时才会被初始化[JLS, 12.4.1]。 以下所示:优化

// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

private static FieldType getField() { return FieldHolder.field; }

当第一次调用getField方法时,它首次读取FieldHolder.field,致使FieldHolder类的初始化。 这个习惯用法的优势在于getField方法不是同步的,只执行属性访问,所以延迟初始化几乎不会增长访问成本。 典型的虚拟机将仅同步属性访问以初始化类。 初始化类后,虚拟机会对代码进行修补,以便后续访问该属性不涉及任何测试或同步。this

若是须要使用延迟初始化来提升实例属性的性能,请使用双重检查(double-check )习惯用法。这个习惯用法避免了初始化后访问属性时的锁定成本(条目 79)。这个习惯用法背后的思想是两次检查属性的值(所以得名double check):第一次没有锁定,而后,若是属性没有初始化,第二次使用锁定。只有当第二次检查指示属性未初始化时,才调用初始化属性。因为初始化属性后没有锁定,所以将属性声明为volatile很是重要(第78项)。下面是这个习惯用用法:

// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null) {  // First check (no locking)
        synchronized(this) {
            if (field == null)  // Second check (with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}

此代码可能看起来有点复杂。 特别是,可能不清楚是否须要这个result局部变量。 这个变量的做用是确保field属性在已经初始化的常见状况下只读一次。 虽然不是绝对必要,但这能够提升性能,而且经过应用于低级并发编程的标准更加优雅。 在个人机器上,上面的方法大约是没有局部变量的明显版本的1.4倍。

虽然也能够将双重检查用法应用于静态属性,但没有理由这样作:延迟初始化持有者类习惯用法(lazy initialization holder class idiom)是更好的选择。

双重检查习惯用法有两个变体值得注意。有时候,可能须要延迟初始化一个实例属性,该属性能够容忍重复初始化。若是你发现本身处于这种状况,可使用双重检查的变体来避免第二个检查。毫无疑问,这就是所谓的“单一检查”习惯用法(single-check idiom)。它是这样的。注意,field仍然声明为volatile:

// Single-check idiom - can cause repeated initialization!
private volatile FieldType field;

private FieldType getField() {
    FieldType result = field;
    if (result == null)
        field = result = computeFieldValue();
   return result;

本条目中讨论的全部初始化技术都适用于基本类型以及对象引用属性。 当将双重检查或单一检查惯用法应用于数字基本类型时,根据数字0(数字基本类型变量的默认值)而不是用null来检查属性的值。

若是你不关心每一个线程是否从新计算属性的值,而且属性的类型是long或double之外的基本类型,那么能够选择从单一检查习惯用法中的属性声明中删除volatile修饰符。 这种变体被称为生动的单一检查习惯用法(racy single-check idiom)。 它加速了某些体系结构上的属性访问,但代价是额外的初始化(直到访问该字段的线程执行一次初始化)。 这绝对是一种奇特的技术,不适合平常使用。

总之,应该正常初始化大多数属性,而不是延迟初始化。 若是必须延迟初始化属性以实现性能目标或打破有害的初始化循环,则使用适当的延迟初始化技术。 例如实例属性,使用双重检查习惯用法; 对于静态属性,使用延迟初始化持有者类习惯用法。 能够容忍重复初始化的属性,也能够考虑单一检查习惯用法。

相关文章
相关标签/搜索