Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408
本記事は第11章「並行性」の項目について記載する。
第11章 並行性
項目78 共有された可変データへのアクセスを同期する
複数のスレッドで共有するデータがある場合、スレッド間で同期を取る必要がある。
同期がされない場合、あるスレッドで行ったデータの変更が他のスレッドに伝わらないので不整合が生じる。
データのアクセスを同期するには、アクセッサーメソッドにsynchronized修飾子を追加するか、変数自体にvolatile修飾子を追加する。
また、java.util.concurrent.atomic
で提供されているAtomicクラスを使う方法もある。
volatileは通信の同期だけを提供するが、Atomicクラスはアトミック性も提供しているため、パフォーマンスに優れる。
項目79 過剰な同期は避ける
過剰な同期はパフォーマンス低下、デッドロック、想定外の挙動となる恐れがある。
同期された領域内では、できる限り処理を制限すべきであり、デッドロックとならないように速やかにロックを開放させる。
行う必要のある処理が多ければ領域外で実行するように検討して設計すると良い。
項目80 スレッドよりもエグゼキュータ、タスク、ストリームを選ぶ
スレッドを用いたワークキューを自作するより、java.util.concurrent
で提供されているエグゼキュータフレームワークを使うと良い。
エグゼキュータフレームワークは、柔軟なインタフェースに基づくタスク実行機構である。
次のような機能を利用することができる。
- 特定のタスクやすべてのタスクの完了を待つ(invokeAny, invokeAll)
- エグゼキュータサービスの完了を待つ(awaitTermination)
- タスクが完了したらそのタスクの結果を取り出す(ExecutorCompletionService)
- 特定の時刻や周期的なタスクの実行(ScheduleThreadPoolExecutor)
エグゼキュータサービスの選択については、 軽い負荷のサーバであれば、Executors.newCachedThreadPool
を選択するのは一般的には良い。
ただし、高負荷のサーバであれば、固定数のスレッドを持つプールを提供するExecutors.newFixedThreadPool
が良い。
更に、詳細な設定が必要である場合は、ThreadPoolExecutor
を使うべきである。
項目81 waitとnotifyよりも並行処理ユーティリティを選ぶ
Java5以降、Javaプラットフォームは、高レベルの並行処理ユーティリティを提供している。
そのため、利用が困難であるwaitとnotifyの代わりに、並行処理ユーティリティを利用するべきである。
java.util.concurrent
の高レベルのユーティリティは、3つのカテゴリに分類される。
- エグゼキュータフレームワーク
- コンカレントコレクション
- シンクロナイザ
コンカレントコレクションは、List、Queue、Mapなどの標準コレクションインターフェスの高パフォーマンスな並列実行である。
これらのコレクションは、高い並行性を提供するため、独自の同期を内部的に管理するように実装されている。
シンクロナイザは、スレッド同士が互い持つことを可能にするオブジェクトである。
スレッドが活動を調整できるように実装されている。
項目82 スレッド安全性を文書化する
クラスは、スレッド安全性の特性をスレッド安全性アノテーションを使って文書化するべきである。
スレッド安全性のレベルには以下のようなカテゴリがある。
- 不変(immutable)
該当のクラスのインスンスは、不変であり定数のように見える。 - 無条件スレッドセーフ(unconditionally thread-safe)
該当のクラスのインスタンスは、可変だが、すべてのメソッドは内部同期を含んでいる。 - 条件付きスレッドセーフ(conditionally thread-safe)
該当のクラスのインスタンスは、可変だが、メソッドによっては外部同期を含んでいる。 - スレッドセーフではない(not thread-safe)
該当のクラスのインスタンスは、可変だが、メソッドの呼び出しを外部同期で囲む必要がある。 - スレッド敵対(thread-hostile)
該当のクラスは、すべてのメソッドの呼び出しが外部同期で囲まれていたとしても、並行実行する上で安全ではない。
これらのカテゴリに対応させるスレッド安全性のアノテーションは次のとおり。
- Immutable
- ThreadSafe
- NotThreadSafe
項目83 遅延初期化を注意して使う
遅延初期化は、フィールドの値の初期化を必要となる直前まで遅らせる手法のことである。
この手法は最適化のために行われるが、必要がなければするべきではない。
なぜなら、遅延初期化されたフィールドへのアクセスコストは増大するのとともに、
遅延初期化対象のフィールドの選定やアクセス頻度などによっては、かえってパフォーマンスが悪くなる可能性があるから。
そのため、大体の場合において普通に初期化したほうが良い。
ただし、パフォーマンス目標を達成するなどの理由がある場合は、適切に遅延初期化を使うことも問題ではない。
二重チェックイデオム(2回検査)を使うことで、フィールドが初期化された後にアクセスされた場合のロックのコストを避けることができる。
項目84 スレッドスケジューラに依存しない
アプリケーションの正しさについて、スレッドスケジューラに依存するべきではない。
依存したアプリケーションは、移植可能ではなくなる。
最善の方法は、実行可能なスレッドの平均数を、プロセッサの数よりも大きくしないこと。