遅延初期化(lazy initialization)とは、メンバフィールドやメンバフィールドが参照するオブジェクトの生成を、インスタンスが実際に必要になるまで延期することである。これにより、クラスのコンストラクタにおいて、フィールドの値を計算したり参照されるオブジェクトを生成したりする必要がなくなる。また、クラスやインスタンスの循環初期化を防止することにもつながり、その他の最適化も可能になる。
遅延初期化にはクラスメソッドあるいはインスタンスメソッドが用いられる。どちらが用いられるかはメンバオブジェクトが static であるかどうかによる。メソッドは、インスタンスが生成済であるかどうかを確認し、生成されていない場合にはインスタンスを生成する。インスタンスが生成済みの場合、単純にインスタンスを返す。
// 遅延初期化をシングルスレッドで行う、正しいバージョン final class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { helper = new Helper(); } return helper; } // ... }
マルチスレッドアプリケーションでは、複数のスレッドがメンバオブジェクトの余計なインスタンスを作成しないように、遅延初期化処理を同期しなくてはならない。
// 遅延初期化をマルチスレッドで行う、正しいバージョン final class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) { helper = new Helper(); } return helper; } // ... }
「ダブルチェックロック手法」は、同期する対象を、フィールドの値の計算やフィールドが参照する新規インスタンスの生成といった頻度の低い処理に制限し、かつ、生成済みインスタンスや値の取得といった頻度の高い処理中には同期を行わないことによって、実行性能を改善する。
「ダブルチェックロック手法」を誤用するケースとして、未初期化あるいは初期化が完了していないオブジェクトの公開を許してしまうことがある。ダブルチェックロック手法は、helperオブジェクトへの参照と、Helperクラスのインスタンスを完全に生成する処理のどちらにおいても事前発生関係(happens-before)を正しく確立するような形でのみ使うべきである。
違反コード
ダブルチェックロック手法を使った以下のコードでは、メソッド同期ではなくブロック同期を用い、同期を行う前にnullチェックを追加している。これは、ダブルチェックロック手法の誤用例である。
// 「ダブルチェックロック」 final class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } // 他のメソッドやメンバを定義... }
Pughは次のように述べている。[Pugh 2004]
Helperオブジェクトを初期化する書込みとhelperフィールドへの書込みは、意図せぬ順序で実行されうる。getHelper()メソッドを呼び出すスレッドでは、nullではないhelperオブジェクトへの参照を得ることはできるが、helperオブジェクトの各フィールドに関しては、コンストラクタが初期化した値ではなく、各フィールドのデフォルト値が参照される可能性がある。
コンパイラがこれらの書込み順序を変更しない場合であっても、マルチプロセッサ環境下では、プロセッサまたはメモリシステムがこれらの書込み順序を変更するかもしれない。
このコードは「TSM03-J. 初期化が完了していないオブジェクトを公開しない」にも違反している。
適合コード (volatile 変数)
以下の適合コードでは、helperフィールドをvolatile宣言している。
// volatile変数の獲得/解放セマンティックスに基づき機能する // JDK 1.4以前のバージョンでは機能しない final class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } }
あるスレッドがHelperオブジェクトを初期化すると、Helperオブジェクトを初期化するスレッドとインスタンスを返す別のスレッドとの間に、事前発生関係が確立される [Pugh 2004、Manson 2004]。
適合コード (static 変数として初期化)
以下の適合コードでは、helperフィールドを static 変数として宣言し、同時に初期化している。
final class Foo { private static final Helper helper = new Helper(); public static Helper getHelper() { return helper; } }
static宣言された変数で、宣言と同時に初期化あるいは静的初期化子(static initializer)によって初期化されるものは、他のスレッドに可視となる前に、初期化が完了していることが保証されている。ただし、遅延初期化の恩恵を受けることはできない。
適合コード (Initialize-On-Demand Holder クラスパターン)
以下の適合コードは Initialize-On-Demand Holder クラスパターンを使用している。このパターンでは、内部クラスであるHolderの中で静的変数を宣言することで、暗黙的に遅延初期化処理が行われる。
final class Foo { // 遅延初期化処理 private static class Holder { static Helper helper = new Helper(); } public static Helper getInstance() { return Holder.helper; } }
static宣言したhelperフィールドの初期化は、getInstance()メソッドが呼び出されるまで遅延される。必要とされる事前発生関係は、クラスローダーがHolderインスタンスをロードし初期化する動作と、Javaメモリモデルが提供する保証によって、確立される。static宣言したフィールドの遅延初期化を行う場合、この手法はダブルチェックロックよりも優れている。[Bloch 2008]しかし、この手法を使ってインスタンスフィールドの遅延初期化を行うことはできない [Bloch 2001]。
適合コード (ThreadLocalストレージ)
以下の適合コード(オリジナルはAlexander Terekhovが提示[Pugh 2004])ではThreadLocalオブジェクトを使って、事前発生関係が確立される同期に各スレッドが参加したかどうかを把握する。各スレッドは、同期する createHelper()メソッドの中でだけ、nullでない値をスレッドローカルなperThreadInstanceに保存する。perThreadInstance.get() が null を返す場合には、createHelper()を呼び出すことで必要な事前発生関係を確立しなくてはならない。
final class Foo { private final ThreadLocal<Foo> perThreadInstance = new ThreadLocal<Foo>(); private Helper helper = null; public Helper getHelper() { if (perThreadInstance.get() == null) { createHelper(); } return helper; } private synchronized void createHelper() { if (helper == null) { helper = new Helper(); } // non-null値をset()メソッドの引数として使う perThreadInstance.set(this); } }
適合コード (不変クラス)
以下の適合コードにおいて、Helper クラスは不変であると仮定する。不変オブジェクトは他のスレッドに可視になる前に生成が完全に完了していることが、Javaメモリモデル(JMM)によって保証されている。さらに、getHelper()メソッド内でブロック同期を行うことで、helperフィールドにnullではない値がはいっているすべてのスレッドが、helper参照の更新に関して適切な事前発生関係を持っていることが保証される。この同期とJavaメモリモデルによる保証によって、perThreadInstanceにnullでない値がはいっているスレッドに対しては、完全に初期化されたHelperオブジェクトだけが可視になることが保証される。このようにして、この解決法では、必要とされる2つの事前発生関係が両方とも正しく確立される。
public final class Helper { private final int n; public Helper(int n) { this.n = n; } // 他のフィールドやメソッドは、すべてfinal宣言される } final class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(42); } } } return helper; } }
例外
LCK10-EX0:32ビットのプリミティブ変数(intやfloat)に対してであれば、違反コードに示したようなダブルチェックロック手法を用いることは可能である。ただし、推奨はされない。[Pugh 2004] この場合、プリミティブ値が初期化されたスレッド間には、必要な事前発生関係が成り立つ。2番目の(参照されるフィールドの初期化のための)事前発生関係は、実質的には無用である。なぜなら、32ビット以下のプリミティブ値の同期されていない読書きはアトミックであることが保証されているからである。したがって、32ビットのプリミティブ変数に対しては、必要な事前発生関係は確立されることになる。しかし、longやdouble型に対してはこの例外は当てはまらない。なぜなら、64ビットのプリミティブ変数への同期していない読書きのアトミック性は保証されておらず、すべてのスレッドが完全に初期化された64ビット値を見るようにするためには、2番目の事前発生条件が必要になるからである(詳しくは、「VNA05-J. 64ビット値の読み書きはアトミックに行う」を参照)。
リスク評価
ダブルチェックロック手法を誤用すると、同期処理に関する問題が発生し、初期化を完了していないオブジェクトを公開することにつながる。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
LCK10-J | 低 | 中 | 中 | P4 | L3 |
関連ガイドライン
MITRE CWE | CWE-609. Double-checked locking |
参考文献
[API 2006] | |
[Bloch 2001] | Item 48. Synchronize access to shared mutable data |
[Bloch 2008] | Item 71. Use lazy initialization judiciously |
[JLS 2005] | §12.4, Initialization of Classes and Interfaces |
[Pugh 2004] |
翻訳元
これは以下のページを翻訳したものです。
LCK10-J. Do not use incorrect forms of the double-checked locking idiom (revision 121)