お題
「Javaによる関数型プログラミング」のP.133に登場する遅延初期化用のHolderというクラスがあります。本文ではこのクラスはスレッドセーフだとされています。お題は「このHolderクラスは本当にスレッドセーフなのか?」です。
Holderクラスをまるごと掲載すると著作権的な問題がありそうなので、気になる人は
- http://media.pragprog.com/titles/vsjava8/code/lazy/fpij/Holder.java を参照するか
- https://pragprog.com/titles/vsjava8/source_code からアーカイブをダウンロードして、lazy/fpij/Holder.javaを参照してください。
Holderがスレッドセーフかどうかを考える上で、気になるのは以下の2点です。
- コンストラクタでのheavyの初期化
- createAndCacheHeavy()の中でのheavyの書き換え
コンストラクタでのheavyの初期化
ここは残念ながらスレッドセーフと言えないでしょう。Javaの言語仕様上、finalでもvolatileでもないフィールドの初期化は他のスレッドから見えることは保証されていないはずです。つまり他のスレッドからはheavyはnullにみえる可能性があります。
ただ現実問題として、どこかで生成されたHolderインスタンスを複数のスレッドで共有する場合、大概は共有の過程のなかでmutexなどを取ったりするので、この点に間しては目をつぶるという選択肢もありえると思います。
createAndCacheHeavy()の中でのheavyの書き換え
さて問題はこちらです。一応、前提として、コンストラクタでのheavyの初期化は、どのスレッドにも見えていることとします。
本文では例としてHolderにアクセスする3つのスレッドが挙げられています。
- 最初にcreateAndCacheHeavyを実行するスレッド
- 上記スレッドが終わるまで、createAndCacheHeavyのロックがとれるのを待っているスレッド
- 上記2つのスレッドよりもあとにHolder#getHeavy()を呼び出すスレッド
最初の2つのスレッドは、createAndCacheHeavyのsynchronizeで同期しているので、特に問題ありません。2番目のスレッドは、書き換え後のheavyの値を正しく見ることができます。
問題は3番目のスレッドです。このスレッドがHolder#getHeavy()を呼んだ時にフィールドheavyはどこを指しているでしょうか。考えられる可能性は3つです。
- コンストラクタで設定した () -> createAndCacheHeavy();
- 最初のスレッドが設定した、heavy = new HeavyFactory();
- 上記、どちらでもない不定値
結論から言えば、上2つのいずれかと思われます。言語仕様、
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7
によれば、
Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
とのことなので、3番目の可能性はなさそうです。(Thanks to torutk)
ただし、コンストラクタで設定した () -> createAndCacheHeavy(); が見えてしまう可能性はあります。たとえ、時系列的に最初のスレッドがheavyを書き替えた後でも、3番目のスレッドがロックを取ってない以上、最新の値が可視になる保証はないからです(atomicであることと、可視であることは違う)。
このクラスのうまいところは、3番目のスレッドが
- 仮にheavyの古い値を見てしまったとしても、結局 createAndCacheHeavy()で同期される
- heavyの新しい値を見た場合は、HeavyFactoryがスレッドセーフなので正しい値がgetできる
ということです。
まとめ
個人的には、この手のロックの取得をスキップする試みは後になって、欠点が見つかることがよくあるので、実戦で使って良いものかどうか少し迷うところです。
余談
このHolderクラスですが、内部的に new Heavy() を呼んでいるので、これ自体を再利用することはできません。しかしHolderのコンストラクタで、Supplier<T>をもらって、new Heavy()を supplider.get() に置き換えれば、汎用ライブラリにすることができます。