先のエントリで
もりあがったdouble-checked-locking問題についてのまとめです。
参考資料
IBMの記事が質が高くて分かりやすいですね。
double-checked locking問題とは
「double-checked lockingとSingletonパターン」では、GoFデザインパターンのSingletonパターンを
Javaで実装する際に同期の処理コストも含め、どのようにすべきかについて書かれています。
「double-checked locking」と呼ばれるアルゴリズムは、下記のようなコードです。
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
このコードは、同期による処理コストも少なく、かつ正しく同期され、Singletonが実装されているように見えます。
しかし、思いもよらない問題が顔を出します。
このgetInstance()返されるインスタンスはnullではないにも関わらず、
コンストラクタによる初期化が終わっていない可能性がある、という話です。
べつに同期自体がうまくいかず、複数回インスタンスが生成されるというわけではありません。
メモリを確保し、参照が作られ、その後にコンストラクタが動く、
その流れのなかで、コンストラクタが動く前に参照がreturnされることがある、
ということをIBMの記事では解説しています。
先のエントリのコメントでは、この参照は作られたが初期化がされていないタイミングを逢魔が刻とたとえたのでした。
これは、最適化などのために処理が変わらない範囲での順序の変更が許されているという
VM実装の事情によるもののようです。
これがメモリの同期と絡まって出てきた逢魔が刻こそが
double-checked locking問題ということになります。
JSR 133
Javaの仕様はJCP(Java Community Process)という機関で決められます。
ここで議論される要求がJSR(Java Specification Reqest)です。このあたりのプロセスは
Javaの開発者は,ちょっと“うるさい”
という記事が分かりやすいかと思います。
JSR 133はJavaのメモリモデルに対する仕様変更要求です。
「コンパイラ開発者のためのJSR133クックブック」がこのJSR 133について詳しく解説しています。
今回参考資料に挙げたリンクは原文と日本語との対訳で書かれているので読みやすいと思います。
このJSR 133はJavaSE 5.0から搭載されています。
問題は解決するのか?
JSR 133によって、volatileなフィールドに関しては、
参照は作られたが初期化がされていない逢魔が刻は存在しないようです。
この点についてはIBMの記事に解説があるので、ちょっと長いですが引用します。
これはdouble-checked locking問題を解決するのか?
double-checked locking問題に対して提案されている解決方法の一つは遅延初期化されたインスタンス(lazily initialized instance)を保持するフィールドをvolatileフィールドにするというものです。(double-checked locking問題と、その解決方法として提案されたアルゴリズム的な方法ではなぜうまく行かないかの説明については参考文献を見てください。)古いメモリ・モデルの下では、これではdouble-checked lockingをスレッド・セーフにしませんでした。その理由はvolatileフィールドへの書き込みは、他の非volatileフィールド(例えば新しくコンストラクトされたオブジェクトのフィールドなど)への書き込みで、やはりリオーダーでき、そのためvolatileインスタンス参照は、不完全にコンストラクトされたオブジェクトへの参照をやはり保持できたためです。
新しいメモリ・モデルの下では、double-checked lockingに対するこの「解決方法」で表現法(idiom)がスレッド・セーフになるのです。それにもかかわらず、まだこの表現法を使うべきではないのです! Double-checked lockingの要点は、ごく初期のJDKでは同期化が比較的高価だったという大きな理由から、共通コード・パスの同期化を不要にするために考えられた、パフォーマンス最適化のはずだった、ということなのです。その後、非競合同期化(uncontended synchronization)はずっと安価になったのですが、volatileの意味体系に加えられた新しい変更によって、一部のプラットフォームでは古い意味体系よりも比較的高価になってしまったのです。(実質的には、volatileフィールドへの各読み書きはちょうど、「半」同期化のようなものです。つまりvolatileの読み込みはモニターが取得するのと同じメモリ意味体系を持ち、volatileへの書き込みはモニターが解放するのと同じ意味体系を持っているのです。)ですから、double-checked lockingの目標が、より単純な、同期化による手法よりも改善されたパフォーマンスを得る事だとすると、この「修正版」解決方法もあまり役には立たちません。
このように、volatileである場合はスレッドセーフになりうると書かれています。
ではなぜ、単なるdouble-checked lockingでは駄目なのでしょう?
メモリモデルが変更になったのだから、synchronizedでちゃんと同期してくれるようになったんじゃないの?
私の理解では、volatileでもsynchronizedでも、メモリの同期は行われるわけですが、
volatileフィールドへの代入に際しては最適化のための順序置き換えが禁止されるため、
逢魔が刻を回避できる、単なるsynchronizedによるメモリ同期ではそれを回避できない、という解釈です。
このあたりの結論は自信がないので、是非、コメントで突っ込みを入れてください。
結論
結局のところ、JavaSE 5.0だとしても、冒頭に掲げたサンプルコードでは初期化されていない
参照が返される可能性があるということになるのではないでしょうか。
volatileフィールドを利用することで回避することはできるのでしょうが、
同期のコストを回避するというdouble-checked lockingの本来の目的は達成できない、
という結論となるようです。
投稿日時 : 2007年8月24日 9:52