第七十一条 慎用延迟初始化

延迟初始化是延迟到需要域的值时才将它初始化的这种行为。如果永远不需要这个值,那么这个域就永远不会被初始化,这种方法既适用于静态域,也适用于实例域。延迟初始化主要是一种优化方式。

就像大多数的优化一样,对于延迟初始化,最好建议“除非绝对必要,否则就不要这么做”。延迟初始化就像一把双刃剑。它降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。根据延迟初始化的域最终需要初始化的比例、初始化这些域要多少开销,以及每个域多久被访问一次,延迟初始化实际上降低了性能。它实际上是把一开始就可以初始化的数据延迟了,算是时间延后了再去执行数据的初始化,严格来说,这个算不上是优化。延迟初始化有它的好处,但我们要测量延迟和非延迟初始化的性能差异。

当有多个线程,并发的情况下,延迟初始化是需要技巧的。如果两个或者多个线程共享一个延迟初始化的域,采用某种形式的同步是很重要的,否则就可能造成严重的Bug,造成数据不同步。在大多数情况下,我们应该谨记:

在大多数情况下,正常的初始化要优先于延迟初始化。

如果利用延迟优化来破坏初始化的循环,就要使用同步访问方法,因为它是最简单、最清楚地替代方法。


比如说,下面这段代码

 一 
    private final FieldType field = computeFieldValue();

二   
    private FieldType field;

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

我们用了同步锁来保证同步,但效率问题呢,加锁的一般比不加锁的效率都要低,如果非延迟的话,性能效率可能会更高吧。

如果从性能角度考虑,可以使用 lazy initialization holder class 模式,这种是建立 static 静态域,对它进行延迟初始化。

    private static class FieldHolder {
        static final FieldType field = computeFieldValue();
    }
 
    public static FieldType getField() {
        return FieldHolder.field;
    }

第一次调用getField()方法时,读取FieldHolder的静态域,导致静态field的初始化,这种方式的好处是不用对它加锁,getField()方法不需要同步。这是用了静态内部类在使用的时候才进行初始化这个特点。但这种模式也有缺陷,如代码所示,上面的方法都必须是静态,应为非静态方法无法调用静态方法,并且通过类直接引用类的属性,也只有静态成员变量才支持这个功能。如果把上述的静态修饰词去掉,上述代码根本无法通过编译。

如果出于性能的考虑而需要对实例域使用延迟初始化,就是用双重检查模式。这种模式背后的思想是:两次检查域的值,第一次检查时没有锁定,看看这个域是否被初始化了;第二次检查时有锁定。只有当第二次检查时表明这个域没有被初始化,才会调用computeFieldValue方法对这个域进行初始化。因为如果域已经被初始化就不会有锁定,域被声明为volatile很重要。

    private volatile FieldType field;

    public FieldType getField() {
        FieldType result = field;
        if (result == null) {
            synchronized (this) {
                result = field;
                if (result == null) {
                    field = result = computeFieldValue();
                }
            }
        }
        return result;
    }

只要该域被初始化以后,无论如何再也不会进入第二次的条件语句判断,不会被synchronized锁定,只有为null时,才会进入下一层判断,此时同步锁住后,再次判断是否有值。这个里面用到了一个局部变量,有人或许有疑惑,说可以省略,为什么要用它呢。

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

result局部变量的使用,是为了保证在已经被初始化的情况下,原来的变量只被读取一次到局部变量result中,最后return 时直接把它返回;按照下面省略局部变量的情况,在比较是否为null的时候需要读取一次,返回的时候还需要读取一次。相比较而言,增加个局部变量会稍微提高些性能。

对于上述,介绍大概就这么多了。通过上面的介绍,有没有很熟悉的感觉,单例模式啊!我们单例模式有 饿汉式和懒汉式,具体写法有加锁的,有静态域的,甚至还有用枚举的,我所知道的单例写法有七种写法,七种除了枚举,其他都是通过技术手段来实现。单例和本文方法的思路一样,区别是一个作用的是对象,一个作用的是方法。

总之,大多数的域应该正常进行初始化,而不是延迟初始化。如果必须延迟初始化一个域,就可以使用相应的延迟初始化方法。对于实例域,就是用双重检查模式;对于静态域,则使用lazy initialization holder class。对于可以接受重复初始化的实例域,也可以考虑使用单重检查模式。

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/84872321