1. 程式人生 > >第七十一條 慎用延遲初始化

第七十一條 慎用延遲初始化

延遲初始化是延遲到需要域的值時才將它初始化的這種行為。如果永遠不需要這個值,那麼這個域就永遠不會被初始化,這種方法既適用於靜態域,也適用於例項域。延遲初始化主要是一種優化方式。

就像大多數的優化一樣,對於延遲初始化,最好建議“除非絕對必要,否則就不要這麼做”。延遲初始化就像一把雙刃劍。它降低了初始化類或者建立例項的開銷,卻增加了訪問被延遲初始化的域的開銷。根據延遲初始化的域最終需要初始化的比例、初始化這些域要多少開銷,以及每個域多久被訪問一次,延遲初始化實際上降低了效能。它實際上是把一開始就可以初始化的資料延遲了,算是時間延後了再去執行資料的初始化,嚴格來說,這個算不上是優化。延遲初始化有它的好處,但我們要測量延遲和非延遲初始化的效能差異。

當有多個執行緒,併發的情況下,延遲初始化是需要技巧的。如果兩個或者多個執行緒共享一個延遲初始化的域,採用某種形式的同步是很重要的,否則就可能造成嚴重的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。對於可以接受重複初始化的例項域,也可以考慮使用單重檢查模式。