Effective Java 第三版——83. 明智謹慎地使用延遲初始化

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java

Effective Java, Third Edition

83. 明智謹慎地使用延遲初始化

延遲初始化(Lazy initialization)是延遲屬性初始化直到須要其值的行爲。 若是不須要該值,則永遠不會初始化該屬性。 此技術適用於靜態和實例屬性。 雖然延遲初始化主要是一種優化,但它也能夠用來打破類和實例初始化中的有害循環[Bloch05,Puzzle 51]。git

與大多數優化同樣,延遲初始化的最佳建議是「除非須要,不然不要這樣作」(條目 67)。延遲初始化是一把雙刃劍。它下降了初始化類或建立實例的成本,代價是增長了訪問延遲初始化屬性的成本。根據這些屬性中最終須要初始化的部分、初始化它們的開銷以及初始化後訪問每一個屬性的頻率,延遲初始化實際上會下降性能(就像許多「優化」同樣)。github

也就是說,延遲初始化有其用途。 若是僅在類的一小部分實例上訪問屬性,而且初始化屬性的成本很高,則延遲初始化多是值得的。 確切知道的惟一方法是使用和不使用延遲初始化來測量類的性能。編程

在存在多個線程的狀況下,延遲初始化很棘手。若是兩個或多個線程共享一個延遲初始化的屬性,那麼必須使用某種形式的同步,不然會致使嚴重的錯誤(條目 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方法不是同步的,只執行屬性訪問,所以延遲初始化幾乎不會增長訪問成本。 典型的虛擬機將僅同步屬性訪問以初始化類。 初始化類後,虛擬機會對代碼進行修補,以便後續訪問該屬性不涉及任何測試或同步。this

若是須要使用延遲初始化來提升實例屬性的性能,請使用雙重檢查(double-check )習慣用法。這個習慣用法避免了初始化後訪問屬性時的鎖定成本(條目 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;

本條目中討論的全部初始化技術都適用於基本類型以及對象引用屬性。 當將雙重檢查或單一檢查慣用法應用於數字基本類型時,根據數字0(數字基本類型變量的默認值)而不是用null來檢查屬性的值。

若是你不關心每一個線程是否從新計算屬性的值,而且屬性的類型是long或double之外的基本類型,那麼能夠選擇從單一檢查習慣用法中的屬性聲明中刪除volatile修飾符。 這種變體被稱爲生動的單一檢查習慣用法(racy single-check idiom)。 它加速了某些體系結構上的屬性訪問,但代價是額外的初始化(直到訪問該字段的線程執行一次初始化)。 這絕對是一種奇特的技術,不適合平常使用。

總之,應該正常初始化大多數屬性,而不是延遲初始化。 若是必須延遲初始化屬性以實現性能目標或打破有害的初始化循環,則使用適當的延遲初始化技術。 例如實例屬性,使用雙重檢查習慣用法; 對於靜態屬性,使用延遲初始化持有者類習慣用法。 能夠容忍重複初始化的屬性,也能夠考慮單一檢查習慣用法。

相關文章
相關標籤/搜索