EffectiveJava第十章第六节

慎用延迟初始化

延迟初始化作为一种性能优化的技巧,它要求类的域成员在第一次访问时才执行必要的初始化动作,而不是在类构造的时候完成该域字段的初始化。和大多数优化一样,对于延迟初始化,最好的建议"除非绝对必要,否则就不要这么做"。延迟初始化如同一把双刃剑,它确实降低了实例对象创建的开销,却增加了访问被延迟初始化的域的开销,这一点在多线程访问该域时表现的更为明显。见如下代码:

public class TestClass {
        private final FieldType field;
        synchronized FieldType getField() {
            if (field == null) 
                field = computeFieldValue();
            return field;
        }
    }

从上面的代码可以看出,在每次访问该域字段时,均需要承担同步的开销。如果在真实的应用中,在多线程环境下,我们确实需要为一个实例化开销很大的对象实行延迟初始化,又该如何做呢?该条目提供了3中技巧:

  • 对于静态域字段,可以考虑使用延迟初始化Holder class模式:
public class TestClass {
        private static class FieldHolder {
            static final FieldType field = computeFieldValue();
        }
        static FieldType getField() {
            return FieldHolder.field;
        }
    }

getField()方法第一次被调用时,它第一次读取FieldHolder.field,导致FieldHolder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上并没有增加任何访问成本。现在的VM将在初始化该类的时候,同步域的访问。一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步。

  • 对于实例域字段,可使用双重检查模式:
public class TestClass {
        private volatile FieldType f;
        FieldType getField() {
            FieldType result = f;
            if (result == null) {
                synchronized(this) {
                    result = f;
                    if (result == null)
                        f = result = computeFieldValue();
                }
            }
            return result;
        }
    }

注意在上面的代码中,首先将域字段f声明为volatile变量,其语义在之前的条目中已经给出解释,这里将不再赘述。再者就是在进入同步块之前,先针对该字段进行验证,如果不是null,即已经初始化,就直接返回该域字段,从而避免了不必要的同步开销。然而需要明确的是,在同步块内部的判断极其重要,因为在第一次判断之后和进入同步代码块之前存在一个时间窗口,而这一窗口则很有可能造成不同步的错误发生,因此第二次验证才是决定性的。
  在该示例代码中,使用局部变量result代替volatile的域字段,可以避免在后面的访问中每次都从主存中获取数据,从而提高函数的运行性能。事实上,这只是一种代码优化的技巧而已。
  针对该技巧,最后需要补充的是,在很多并发程序中,对某一状态的测试,也可以使用该技巧。

  • 对于可以接受重复初始化实例域字段,可使用单重检查模式:
public class TestClass {
        private volatile FieldType f;
        FieldType getField() {
            FieldType result = f;
            if (result == null)
                f = result = computeFieldValue();
            return result;
        }
    }

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

你可能感兴趣的:(EffectiveJava第十章第六节)