Javaの2重チェックイディオムとhappens-before関係

The "Double-Checked Locking is Broken" Declarationを斜め読みしたところ、Javaで遅延初期化をやるときに2重チェックイディオムが使えるかどうかで悩んだのでメモ。
 
以下のようなクラスがあったとする。
public class AClass {
     private AHugeClass final value; // 状態を持つ巨大なオブジェクト

     public AClass() {
          value = new AHugeClass(); // かなり膨大で副作用のある計算
          /* 他の処理 */
     }

     public AHugeClass getValue() {
          return value;
     }
}
 
基本的にはこのコードに何も問題はないが、value変数の初期化に非常に多くの計算が必要で、かつ場合によってはvalue変数を一切使わないようなとき(計算が無駄になるとき)には、あまり良いコードとは言えない。
 
そこで、遅延初期化を行う。
遅延初期化は、初期化の計算が膨大で、初期化を遅らし、必要なければ初期化を行わないための手法のこと。
 
これをそのまま実装すると、以下のようになる。
public class AClass {
     private AHugeClass value;

     public AClass() {
          /* 他の処理 */
     }
     public AHugeClass getValue() {
          if(value == null) value = new AHugeClass(); // かなり膨大で副作用のある計算
          return value;
     }
}
 
こうするとスレッドが1つのときは正しく遅延初期化されるが、マルチスレッド環境ではdoAHugeCalculation()が複数回呼ばれてしまう。副作用がなければ何度よばれても別に問題ないが、副作用がある場合は以下のような排他制御が必要になる。
public class AClass {
     private AHugeClass value;

     public AClass() {
          /* 他の処理 */
     }
     public synchronized AHugeClass getValue() {
          if(value == null) value = new AHugeClass(); // かなり膨大で副作用のある計算
          return value;
     }
}
 
しかしsynchronizedを使うと、多くのスレッドがアクセスする場合にはメソッドへのアクセス速度が激しく低下してしまう。
そこで、volatile変数で原子性を確保しつつ、2回の判定でなるべく排他しないようにしたものが2重チェックイディオム。
public class AClass {
     private AHugeClass value;

     public AClass() {
          /* 他の処理 */
     }
     public AHugeClass getValue() {
          if(value == null) {
               synchronized(this) {
                    if(value == null) {
                         value = new AHugeClass(); // かなり膨大で副作用のある計算
                    }
               }
          }
          return value;
     }
}
 
これで晴れて遅延初期化を実装できた。
 
しかし、このコードが正しく動作するのはJDK5以降のみ。
JDK1.4以前では、volatile変数にhappens-before関係が存在しないため、2重チェックイディオムをすり抜けてしまう。
※JDK1.4以前のvolatileは、変数がスレッドごとのキャッシュを読まないということのみ保証している。
 
具体的にどう抜けるのかはThe "Double-Checked Locking is Broken" Declarationが詳しいが、根本的な原因はプログラムが書いた順番通りには動かない(逐次一貫性が保証されない)ことである。
上の例では、value = new AHugeClass();でvalueにAHugeClassのインスタンスを代入したにAHugeClassのコンストラクタが呼ばれている。
 
happens-before関係(happened-before、C++流にはsequenced-beforeともいう。日本語では「前に発生」)は、ある命令がプログラムに書かれた順序で実行されるという関係。
普段プログラミングをしているときにはこれは当然のもののように感じるが、内部的には徹底的な最適化が行われているので、プログラムが書いた順に実行されるということはまずない。
happens-before関係を保証するのがC++やJDK5以降のJavaのvolatile修飾子。
 
詳しいことはC++0x Memory Model 第1回 - 1.9 Program execution - Cry’s Diaryを読むとわかるかもしれない。C++の話だし難解だけど。