Effective Java 第三版——83. 明智謹慎地使用延遲初始化
Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
註意,書中的有些代碼裏方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。
83. 明智謹慎地使用延遲初始化
延遲初始化(Lazy initialization)是延遲屬性初始化直到需要其值的行為。 如果不需要該值,則永遠不會初始化該屬性。 此技術適用於靜態和實例屬性。 雖然延遲初始化主要是一種優化,但它也可以用來打破類和實例初始化中的有害循環[Bloch05,Puzzle 51]。
與大多數優化一樣,延遲初始化的最佳建議是“除非需要,否則不要這樣做”(條目 67)。延遲初始化是一把雙刃劍。它降低了初始化類或創建實例的成本,代價是增加了訪問延遲初始化屬性的成本。根據這些屬性中最終需要初始化的部分、初始化它們的開銷以及初始化後訪問每個屬性的頻率,延遲初始化實際上會降低性能(就像許多“優化”一樣)。
也就是說,延遲初始化有其用途。 如果僅在類的一小部分實例上訪問屬性,並且初始化屬性的成本很高,則延遲初始化可能是值得的。 確切知道的唯一方法是使用和不使用延遲初始化來測量類的性能。
在存在多個線程的情況下,延遲初始化很棘手。如果兩個或多個線程共享一個延遲初始化的屬性,那麽必須使用某種形式的同步,否則會導致嚴重的錯誤(條目 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
方法不是同步的,只執行屬性訪問,因此延遲初始化幾乎不會增加訪問成本。 典型的虛擬機將僅同步屬性訪問以初始化類。 初始化類後,虛擬機會對代碼進行修補,以便後續訪問該屬性不涉及任何測試或同步。
如果需要使用延遲初始化來提高實例屬性的性能,請使用雙重檢查(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)。 它加速了某些體系結構上的屬性訪問,但代價是額外的初始化(直到訪問該字段的線程執行一次初始化)。 這絕對是一種奇特的技術,不適合日常使用。
總之,應該正常初始化大多數屬性,而不是延遲初始化。 如果必須延遲初始化屬性以實現性能目標或打破有害的初始化循環,則使用適當的延遲初始化技術。 例如實例屬性,使用雙重檢查習慣用法; 對於靜態屬性,使用延遲初始化持有者類習慣用法。 可以容忍重復初始化的屬性,也可以考慮單一檢查習慣用法。
Effective Java 第三版——83. 明智謹慎地使用延遲初始化