マルチスレッドについていろいろ考えてる機会が多かったのでちょっとまとめてみた。
Singletonのdouble-checked lockingはアンチパターンだという話
double-checked lockingとSingletonパターン
ネットでも各所に書かれているけれど、メジャーな書き方なのでみんないつの間にか使ってしまっていることが多い。*1被害に会う可能性が非常に低いので対策はほぼ必要ないけど知っておいて損はない。
double-checked lockingとはどんなソースかというと
public static Singleton getInstance(){ if (instance == null){ synchronized(Singleton.class) { //1 if (instance == null) //2 instance = new Singleton(); //3 } } return instance; }
至極一般的に見かける外側で1回NULLチェックをしてシンクロ内部でもう1回NULLチェックしてインスタンスを返却するといった実装です。
一見まったく問題ないのですが、刹那のタイミングですがgetInstanceが不正な値を返却する可能性があります。
詳しくはここを参照ですが、メモリが確保される瞬間と、コンストラクタが終了するタイミングが異なるため、その間にスレッドが切り替わる可能性があります。その場合後から来たスレッド(スレッド2)は初期化が完了していないインスタンスを参照してしまいます。
1. スレッド1がgetInstance() メソッドに入ります。
double-checked lockingとSingletonパターン
2. instance がnull であるため、スレッド1は //1でsynchronized ブロックに入ります。
3. スレッド1は //3に進んでインスタンスを非null にしますが、このときにはまだ コンストラクターが実行されていません。
4. スレッド1がスレッド2に取って代わられます。
5. スレッド2は、インスタンスがnull になっているかどうかを調べます。インスタンスがnull になっていないため、スレッド2は、完全に構成され、しかし部分的にしか初期化されていないSingleton オブジェクトを、instance 参照に対して戻します。
6. スレッド2がスレッド1に取って代わられます。
7. スレッド1は、コンストラクターを実行してそれに対する参照を戻すことにより、Singleton オブジェクトの初期化を完了します。
これはJavaのメモリモデルの問題で実際にこれが発生するかはVMやJITコンパイラなどとも関係します(でもJITされる頃にはSingletonオブジェクトの生成は終わってるので問題ない)
C++でMutexで排他せずにvolatileだけで排他したばあいコンパイラによる最適化は防げてもハードウエアによる最適化(命令のreorder)は防げないっていうのと同じようなレベルの話ですね。
java.text.SimpleDateFormat はスレッドセーフではないという話。
これも有名ですね。APIドキュメントにもかいてあります
Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
ついでに「java.text.Format」も「java.text.MessageFormat」も「java.text.NumberFormat」のフォーマット系クラスはスレッドセーフではありません。
JavanoCoreAPIでもスレッドセーフじゃないものはあるので気をつけなくてはなりません。ベンダのAPIやオープンソースの場合は特に気をつけましょう。
インクリメントもスレッドセーフではないという話。
インクリメントは実際には読み取りと追加と保存の3つの演算なのでその間にスレッドが切り替わる可能性があります。
連載.NETマルチスレッド・プログラミング入門:第3回 マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(前編) ― (3/3) - @IT
複数のスレッドから1つのIntなりをインクリメントしまくるソースを書くと再現できたりする。(昔は再現できたのですが今やってみたら再現しなくなってました。VMバージョンアップしてかわったんか?)
StringBufferはスレッドセーフでとStringBuilderはスレッドアンセーフという話。
まあこれはもともとがスレッドセーフだったものはスレッドセーフじゃないものが出来たのでみんな知ってるでしょう。
無駄にスレッドセーフにすると重くなるので必要なところ意外は排他しないようにしましょうという話。
*1:私も3年ぐらい前に教えてもらうまでまったくしらんかった。