Javaとスレッドセーフ
前回、メモリリークについて取り上げました。 メモリリークはプログラマ個々人の意識と技量に依存した扱いが難しい問題であることについて触れました。
メモリリークと同様に難しいのがスレッドセーフです。
スレッドセーフを試験で確認するのはメモリリーク以上に困難です。 不可能といってもよいでしょう。 設計やコードレビューで防ぐしかありません。
スレッドセーフは複雑なトピックです。今回はスレッドセーフの基本的な考え方を取り上げます。
(それでも長文になってしまった。。。)
スレッドセーフとは
「スレッドセーフ」とはマルチスレッドプログラムの評価基準の一つです。
マルチスレッドプログラムの評価基準には「安全性」、「 生存性」、「再利用性」、「パフォーマンス」の4つの基準があります。
このうち「安全性」とはオブジェクトを壊さないことです。
複数のスレッドが利用しても安全性が保たれることを「スレッドセーフ」と言います。
具体的にはフィールドの値を壊さないことです。
加えて、あるスレッドが変更したフィールドの値が他のスレッドから見えることを保証することも安全性の重要な要件です。
Javaのメモリモデルではスレッドが実施した変更はすぐには共有メモリ(ヒープ)に反映されず、一旦、スレッドのキャッシュに反映されます。 このままでは他スレッドが変更内容を無視した処理を実施してしまいます。 スレッド間の可視性を保証することが重要になります。
レベル
世の中の全てのJavaクラスがスレッドセーフなわけではありません。
例えばJavaプログラマならみんな大好きなArrayListはスレッドセーフではありません。
またスレッドセーフの徹底度合いにもレベルがあります。
Effective Javaの項目82ではスレッドセーフのレベルを以下の4つに分類しています。
1. immutable
- immutableなクラスは変更できないのでスレッドセーフです。
- 呼び出し側のクラスでは同期を行わずにこのクラスを利用することができます。
2. unconditionally thread safe
- クラス内部で同期を行ってスレッドセーフにしているクラスです。
- 呼び出し側のクラスでは同期を行わずにこのクラスを利用することができます。
3. conditionally thread safe
- 一部のメソッドに関しては呼び出し側のクラスで同期する必要があるクラスです。
- どのメソッドにはどのような同期が必要か文書化されています。
4. not thread safe
- 全般的に呼び出し側のクラスで同期する必要があるクラスです。
- 同期の方法が文書化されています。
5. thread hostile
- 呼び出し側のクラスで同期しても使用することが危険なクラスです。
- 意図して作成されることはなく、プログラマのミスで生まれます。
- 発見した場合は修正するか破棄する必要があります。
スレッドセーフを意識しなくていいとき
プログラミングやコードレビューをしていて、どのようなときにスレッドセーフを意識すべきでしょうか?
まず最初にスレッドセーフを意識しなくていいときから見ていきましょう。
こちらの方が理解しやすいと思います。
1. 非マルチスレッド環境である
非マルチスレッド環境でクラスをスレッドセーフにしても危険なことはありません。しかし性能が犠牲になります。また工数も増えます。
システム開発のリソースは有限ですから、「とりあえず」スレッドセーフにする必要はないと思います。
2. 複数のスレッドからアクセスされるShared Resourceではない
マルチスレッド環境のすべてのオブジェクトが複数のスレッドからアクセスされるわけではありません。むしろ限られたオブジェクトのみ共有されるように設計するでしょう。
設計上、複数のスレッドからアクセスされないオブジェクトについてはスレッドセーフにする必要はないと思います。
3. Immutableである
クラスがImmutableであれば既にスレッドセーフです。
クラスをImmutableにするのは色々な意味でよいプラクティスです。Immutableにする努力は環境を問わず実施すべきだと思います。
4. スレッドがローカル変数のみ利用している
ローカル変数はスレッド固有です。他スレッドと共有されることはないためスレッドセーフです。
ただしメソッド引数でShared Resourceオブジェクトへの参照を受け取っている場合、そのオブジェクトへの読み書きに注意が必要です。そのオブジェクトが他のスレッドと共有されていて、かつスレッドセーフでない場合は同期処理が必要になります。
5. Thread Local
ローカル変数と同じです。
6. フレームワークがスレッドの独立性を保証している
フレームワークがスレッドを管理し、独立性を保証していることがあります。その場合は考慮は不要です。
フレームワークが独立性を保証するために規則や制約を課していることがあります。もしある場合は守りましょう。
スレッドセーフを意識すべきとき
スレッドセーフを意識すべきときを見ていきましょう。
1. マルチスレッド環境で、かつ複数のスレッドからアクセスされるShared Resourceがあり、かつMutableである
「スレッドセーフを意識しなくていいとき」の反対です。
2. データが壊れる箇所がある
前述の1の条件を見たすMutableなクラスでも安全性が脅かされる箇所(データ壊れる可能性がある箇所)がなければスレッドセーフです。
「データが壊れる箇所」とはAtomicではない操作をしているところです。Atomicではない操作は処理中に他のスレッドが途中で割り込んできてデータが壊れる可能性があります。
あるメソッドの中で2つ以上のフィールドを操作/参照している
- それぞれのフィールドに別々のスレッドが書き込んだ値がセットされる可能性がある
long、doubleを操作している
インクリメント演算子やデクリメント演算子を使っている
- 「++」や「--」はAtomicではない
3. スレッドセーフでないAPIを利用している
JavaのCollectionの多くは性能を優先してスレッドセーフな実装になっていません。例えばLinkedListやArrayListがそうです。これらのCollectionはデータの安全性が失われると例外を送出します。しかしそれに頼ったコーディングをするのは危険です。
他にもAPIドキュメントにスレッドセーフでないことが記載されているAPIを利用している場合には考慮が必要です。
4. あるスレッドの操作結果を他スレッドから見たいとき
Javaのメモリモデルではスレッドが実施した変更はすぐには共有メモリ(ヒープ)に反映されず、一旦、スレッドのキャッシュに反映されるのは前述した通りです。
キャッシュの内容がヒープに反映される契機を作らないと永久にスレッドの変更は他のスレッドから見えません。
フィールドをvolatileにするか、フィールド操作をsynchronizedで囲むことで他のスレッドがフィールドへの変更を見えることを保証できます。
これはint型などでも同様です。int型への操作、参照はatomicなので可視性も担保されていると誤解されていることがありますが、AtomicであることとVisibleであることは別です。
5. スレッドセーフな親クラスのメソッドをオーバーライドしているとき
スレッドセーフなメソッドをスレッドアンセーフなメソッドでオーバーライドするとデータが破壊されてしまいます。
スレッドセーフなクラスの継承には危険が伴います。メソッドをオーバーライドするときは考慮が必要です。
6. 具体例
- ドメインオブジェクト
- データオブジェクト
- データ構造を表すオブジェクト
- 要求やコールバックの登録を受け付けるオブジェクト
- WebアプリケーションのControllerクラス
対処
スレッドセーフでない箇所を見つけた場合、どうすればよいでしょう?
1. atomicパッケージ、スレッドセーフなCollectionの利用を検討する
atomicパッケージ、スレッドセーフなCollectionを利用することでsynchronizedやvolatileを使用せずにスレッドセーフにできることがあります。
2. synchronizedで同期する
synchronizedで同期します。
Shared Resource内部で同期する方式と、Shared Resourceを利用する側で同期する方式があります。
Efffective Javaによると利用する側で同期することが基本方針です。Shared Resource内部で同期する方が高い並行性が実現できる場合にのみそれを実施します。
同期は性能に影響します。synchronizedによる同期は必要な箇所に最小限の範囲で適用しましょう。
3. volatileで可視化する
排他制御が不要で他スレッドに対して可視化だけしたい場合はvolatileを利用できます。
フィールドに対してAtomicでない操作を行っている箇所がないか注意深く確認してからvolatileを適用してください。
その他の細かい話
ここまでの章に収まらなかった雑多なトピックをここにまとめます。
synchronized、wait, notifyを避ける
「Effective Java」や「Optimizing Java」などを読んでいるとJavaの方向性としてsynchronized、wait, notifyを使わないコーディングが主流になっていくそうです。atomic、concurrent Collection、synchronizerなどの使い方に慣れる努力をした方がよいようです。
synchronizedブロックの中でオバーライドされたメソッドやコールバックを呼ばない
設計時に意図していなかった例外、デッドロック、データ破壊が起こる可能性があります。
synchronizedブロックの中では最低限の処理しかしないことです。
ロック対象に注意
conditionally thread safeやnot thread safeなShared Resourceクラスを実装する場合、呼び出し側クラスはsynchronizedによる同期を行うためにロックを必要とします。ロックを外部に公開し、APIドキュメントにも明記してください。
unconditionally thread safeなクラスを実装する場合、呼び出し側クラスは同期を行う必要がありません。ロックを公開する必要がないので、外部から取得できないprivateなものにしましょう。不要なデッドロックを防ぐことができます。
呼び出し側クラスを実装する場合、間違ったオブジェクトにロックをかけないことです。スレッドセーフではなくなります。当たり前のことのようですがCollectionの中には「え?そっちをロックするの?」となるものもあります。APIドキュメントに明記されているのでちゃんと見ましょう。
スレッドセーフなデバッグメッセージ
デバッグメッセージがスレッドセーフでないと出力処理中にもデータが更新されて、出力結果自体が信頼できなくなり問題の解析ができなくなります。
Collections.synchronizedLlistでもイテレータは同期が必要
ArrayListはスレッドセーフではありませんが、Collections.synchronizedLlistでスレッドセーフなリストに変換できます。
add、removeをsynchronizedで囲む必要はなくなります。
しかし拡張for文でイテレータを使う場合はsynchronizedで囲む必要があります。
同期メソッドからのみ呼ばれるprivateメソッドの同期は不要
やってもいいですが性能が悪くなります。他から呼ばれないことが保証されているのであれば過剰な同期は避けた方がよいです。
デッドロック
ロックを取得する箇所が複数あるとデッドロックが発生する可能性があり、考慮が必要になります。
対策としてロックを取得する順序を統一することがよく勧められていますが、システムの要件が複雑になるとそれも難しくなります。
そのような場合は処理の起点、ルートとなるオブジェクトを一つ決めて、そのオブジェクトを最初に必ず取得するようにします。規則がシンプルな分、守りやすくなります。
コンストラクタの中からthisを漏らさない
コンストラクタ内からフィールドにthisを設定するとコンストラクタ完了前のオブジェクトが参照できてしまいます。
finalなフィールドはコンストラクタ完了後に可視化される仕様です。 よってコンストラクタ完了前のオブジェクトにアクセスすると壊れたデータにアクセスしてしまう可能性があり危険です。
もしフィールドにthisを設定する要件がある場合は、コンストラクタが終了してからにするよう配慮する必要があります。