延迟初始化(lazy initialization)是延迟到需要域的值时才将它初始化的这种行为。如果 永远不需要这个值,这个域就永远不会被初始化。这种方法既适用于静态域,也适用于实例域。虽然延迟初始化主要是一种优化,但它也可以用来打破类和实例初始化中的有害循环。
就像大多数的优化一样,对于延迟初始化,最好建议“除非绝对必要,否则就不要这么做”( 见第55条:谨慎的进行优化)。延迟初始化就像一把双刃剑。它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域最终需要初始化的比例、初始化这些 域要多少开销,以及每个域多久被访问一次,延迟初始化(就像其他的许多优化一样)实际上降低了性能。
也就是说,延迟初始化有它的好处。如果域只在类的实例部分被访问,井且初始化这个域的开销很高,可能就值得进行延迟初始化。要确定这一点,唯一的办法就是测量类在用和不用延迟初始化时的性能差别。
当有多个线程时,延迟初始化是需要技巧的。如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug(见第66条:同步访问共享的可变数据)。本条目中讨论的所有初始化方法都是线程安全的。
在大多数情况下,正常的初始化要优先于延迟初始化。下面是正常初始化的实例域的一个 典型声明。注意其中使用了final修饰符(见第15条):
如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚的替代方法:
这两种习惯模式(正常的初始化和使用了同步访问方法的延迟初始化)应用到静态域上时保持不变,除了给域和访问方法声明添加了static修饰符之外。
如果出于性能的考虑而需要对静态域使用延迟初始化,就使用 lazy initialization holder class模式。这种模式保证了类要到被用到的时候才会被初始化。如下所示:
当getField方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到 初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟 初始化实际上并没有增加任何访问成本。现代的VM将在初始化该类的时候,同步域的访问。—旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。
如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom)。这种模式避免了在域被初始化之后访问这个域时的锁定开销 (见第 67条:避免过度同步)。这 种模式背后的思想是:两次检査域的值[因此名字叫双重检查(double-check)],第一次检査 时没有锁定,看看这个域是否被初始化了,第二次检査时有锁定。只有当第二次检査时表明这个域没有被初始化,才会调用computeFieldValue方法对这个域进行初始化。因为如果域已 经被初始化就不会有锁定,域被声明为volatile很重要(见第66条:同步访问共享的可变数据)。下面就是这种习惯模式:
这段代码可能看起来似乎有些费解。尤其对于需要用到局部变量result可能有点不解。这个变量的作用是确保field只在已经被初始化的情况下读取一次。虽然这不是严格需要,但是 可以提升性能,并且因为给低级的并发编程应用了一些标准,因此更加优雅。在我的机器上上述的方法比没用局部变量的方法快了大约25%。
在Java1.5之前,双重检査模的功能很不稳定,因为volatile修饰符的语义不够 强,难以支持它。Java1.5发行版本中引入的内存模式解决了这个问题。如今,双重检査模式是延迟初始化一个实例域的方法。虽然你也可以对静态域应用双重检査模式,但是没有理由这么做,因为lazy initialization holder class idiom是更好的选择。
双重检査模式的两个变量值得一提。有时候,你可能需要延迟初始化一个可以接受重复初 始化的实例域。如果处于这种情况,就可以使用双重检査惯用法的一个变形,它省去了第二次检査,没错,它就是单重检查糢式(single-check idiom)。下面就是这样的一个例子。注意field仍然被声明为volatile:
本条目中讨论的所有初始化方法都适用于基本类型的域,以及对象引用域。当双重检查模式(double-check idiom)或者单重检查糢式(single-check idiom)应用到数值型的基本类型域时,就会用0来检査这个域(这是数值型基本变量的默认值),而不是用null。
如果你不在意是否每个线程都重新计算域的值,并且域的类型为基本类型,而不是long或 者double类型,就可以选择从单重检査模式的域声明中删除volatile修饰符。这种变体称之为racy single-check idiom。它加快了某些架构上的域访问,代价是增加了额外的初始化(直到访问该域的毎个线程都进行一次初始化)。这显然是一种特殊的方法,不适合于日常的使用。然而,String实例却用它来缓存它们的散列码。
简而言之,大多数的域应该正常地进行初始化,而不是延迟初始化。如果为了达到性能目标,或者为了破坏有害的初始化循环,而必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就使用双重检查模式(double-check idiom),对于静态域,则使用lazy initialization holder class idiom。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式(single-check idiom)。