120 Star 1K Fork 302

lin-mt/effective-java-third-edition

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
第83项:慎用延迟初始化.md 7.43 KB
一键复制 编辑 原始数据 按行查看 历史

慎用延迟初始化

  *延迟初始化(Lazy initialization)*是延迟到需要域的值的时候才将它初始化的行为。如果永远不需要这个值,这个域就永远不会被初始化。这种方法既适用于静态域,也适用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环[Bloch05, Puzzle 51]。

  就像大多数的优化一样,对于延迟初始化,最好的建议就是“除非绝对必要,否则就不要这么做”(第 67 项)。延迟初始化就像一把双刃剑。它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域最终需要初始化的比例、初始化这些域要多少开销,以及每个域多久被访问一次,延迟初始化(就像其他的许多优化一样)实际上降低了性能。

  也就是说,延迟初始化有它的好处。如果域只在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。要确定这一点,唯一的办法就是测量类在用和不用延迟初始化时的性能差别。

  当有多个线程时,延迟初始化是需要技巧的。如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能出现严重的 Bug(第 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 方法没有被同步,并且只执行访问一个域【的动作】,因此延迟初始化实际上并没有增加任何访问成本。现代的 VM 将在初始化该类的时候,同步域的访问,一旦这个类被初始化,VM 将修补代码,以便后续对该域的访问不会导致任何测试【测试是否已经初始化】或者同步。

  如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom)。这种模式避免了在域被初始化之后访问这个域时的锁定开销(第 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;
}

  本项中讨论的所有初始化方法都适用于基本类型的域,以及对象引用域。当双重检查模式(double-check idiom)或者单重检查模式(single-check idiom)应用到数值类型的基本类型域时,就会用 0 来检查这个域(这是数值类型基本变量的默认值),而不是 null。

  如果你不在意是否每个(every)线程都重新计算域的值,并且域的类型为基本类型,而不是 long 或者 double 类型,就可以选择从单重检查模式的域声明中删除 volatile 修饰符。这种变体称之为racy single-check idiom。它以某些额外的初始化(每个访问该字段的线程最多【初始化】一次)为代价,在某些体系结构上加快了域访问的速度。这显然是一种特殊的方法,不适合在日常中使用。

  简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能的目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用合适的延迟初始化方法。对于实例域,就使用双重检查模式(double-check idiom);对于静态域,则使用 lazy initialization holder class idiom。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)。

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
其他
1
https://gitee.com/lin-mt/effective-java-third-edition.git
git@gitee.com:lin-mt/effective-java-third-edition.git
lin-mt
effective-java-third-edition
effective-java-third-edition
master

搜索帮助

Cb406eda 1850385 E526c682 1850385