Java並行処理プログラミングまとめ - 第1部 基礎編
というわけでいよいよ本格的に並行処理プログラミングについてです。
と言っても基礎知識ではありますがwでも基礎知識は一番大事だと思うので甘く見ないで下さいね!
第2章 スレッドセーフ
オブジェクトのステートに複数のスレッドからアクセスされる場合、それらのアクセスは必ず同期化されていないとダメ!
スレッドセーフとは
複数のスレッドからどのようにアクセスされても正しく動作し、呼び出し側に同期化の為のコードなどが必要ないこと
- スレッドセーフなクラスの作成・修正方法
- ステートへのアクセスを常に同期化する
- ステートに複数のスレッドからアクセスしない
- ステートを不可変にする
- ステートを持たない
アトミック性
複数の間に割り込みの処理などがあってはいけない処理のまとまり。1つの処理として考えなければいけないもの。
- リード・モディファイ・ライト
- ステートの値を読み込む→読み込んだ値を変更する→読み込み元のステートに値を書き込む
- チェック・ゼン・アクト
- ステートの値をチェックし、その値に応じて処理を実行する
- 複合アクション
- 上記2つのような複数の処理の集まりの総称
java.util.concurrent.atomicパッケージには利用頻度の高いアトミックな操作をスレッドセーフに実装したアトミック変数クラス達がある。
ロック
ステートにアクセスする処理などを単一のスレッドしか行えないように他スレッドをブロックする仕組みのこと。
- Java言語自身のロック機能(固有のロック)
- ロックしたいメソッドにsynchronized修飾子を付与
- synchronized(ロックオブジェクト){ ロックしたい処理 }
- ミューテックス
- ロックを1度に1つのスレッドしか所有できないこと
- 再入可
- 所有しているロックが必要な他のメソッド・ブロックに再入できること
ステートのロック
- 複数のスレッドからアクセスする可変なステートは全てのアクセスが同じロックでガードされていなければならない。
- 複数のステートがアトミックな操作に含まれている場合、それらすべての変数が同じロックによってガードされていなければならない。
- これらの同期化の為のポリシーはドキュメントするなどし、正確に伝えなければならない。
生存と実行性能
synchronizedブロックを細かく小さくすると複雑になるが並行性が向上する。しかし、細かくし過ぎてもロックの取得が頻繁に行われ実行性能が失われる場合もあるのでバランスを取る事が必要。
ただし長時間かかる処理はロックのない状態で行うこと。
第3章 オブジェクトを共有する
可視性
同期化のない処理はJVMよって順序変えの可能性があるので順序変更不可な処理やデータは同期化を行い正しく見えるようにする。(ロックは相互排他だけではなく可視性も保証する)
- 揮発性変数
- volatile宣言された変数。レジスタにキャッシュされず、順序変えも行われなくなる。
Javaのlongとdoubleの64ビットの数値は下位・上位各32ビットの2つわけて処理が行われるので、上記のvolatile宣言またはロックのガードが必要
公開と逸出
公開すべきでないステートへの参照がオブジェクトから公開されることを逸出したという。
スレッド拘束
データが1つのスレッドからしかアクセスされないようにすること。例:SwingのEDTやプールに納められたJDBCのConnectionオブジェクト
- その場しのぎのスレッド拘束
- スレッドセーフでないオブジェクトを1つのスレッドからしかアクセスされないようにアプリケーションやユーザコードの実装に責任を持たせたもの。極力してはいけない!
- スタック拘束(スレッド内使用・スレッドローカル使用)
- ローカル変数からしかオブジェクトにアクセスできないようにすること。ただし参照型の参照を公開したら拘束は破れるので注意
java.lang.ThreadLocalクラスはスレッドごとの値を返すスレッド拘束の手助けとなるクラス
不可変性
不可変なオブジェクトは複数のスレッドからアクセスされても変更されないのでスレッドセーフ
- 不可変な条件
- コンストラクト処理後にステートが変更不可
- すべてのステートがfinalで、オブジェクトが正しく構築されている
可変にする必要のないステートはすべてfinalにするのがよい
オブジェクトを安全に公開するための条件
- オブジェクトの参照をstaticイニシャライザで初期化する
- オブジェクトの参照をvolatile宣言されたステートまたはAtomicReferenceに保存する
- オブジェクトの参照を正しく構築されたオブジェクトのfinalなステートに保存する
- オブジェクトの参照をロックによって正しくガードされたステートに保存する
オブジェクトの公開要件
- 不可変オブジェクトは安全に公開できる
- 実質的に不可変なオブジェクトは安全な公開が必要
- 可変オブジェクトは安全な公開とスレッドセーフ性やロックによるガードが必要
オブジェクトを共有する為のポリシー
- スレッド拘束
- リードオンリーの共有
- スレッドセーフな共有
- ガード
第4章 オブジェクトを組み立てる*1
スレッドセーフなクラスを設計する基本要素
- オブジェクトのステートを構成する変数を同定する
- ステート変数の値などを制約する不変項を同定する
- オブジェクトのステートへの並行アクセスを管理するためのポリシーを確立する
- 不変項と事後条件への理解が重要。
- Javaでは必要ないが他言語ではオブジェクトの所有権の考慮も必要。完全に渡すのか、貸すのか、共有するのか。
- データをオブジェクトにカプセル化し、アクセスするメソッドをロックを使って正しく同期する
- インスタンス拘束
- データをオブジェクトに拘束すること
- クライアントサイドロック・外部ロック
- スレッドセーフなクラスが使用しているロックを取得し利用すること
スレッドセーフの委譲
スレッドセーフなクラスをステートとして使い、スレッドセーフの責任をそのステートに委譲すること。ただし、各ステートがそれぞれ独立していないといけない。
既存のスレッドセーフなクラスへの機能の追加について
継承による機能追加は複数のソースファイルに同期化ポリシーが分散し複雑になるので、スレッドセーフ性が破られたりとバグの原因になるので避ける。追加したい場合はラップによる機能追加がベター。
同期化ポリシーのドキュメントについて
クラスのスレッドセーフ性の保証や同期化ポリシーをクライアントやメンテナンスする人の為にしっかりとドキュメントする。絶対!
第5章 並行処理の構築部材
Java標準APIに存在する並行処理プログラミングをするのに最適な部品の紹介。実装がどのような理論に基づいて設計されているのかの説明もあります。
同期化コレクション
一度に一つのスレッドだけがステートにアクセスするように同期化されているコレクション。複合アクションを行う場合にはロックが必要なコレクションもあるので注意が必要。
同期化コレクションのイテレータは即時断念型でイテレーション中にコレクションが変更されると ConcurrentModificationException(RuntimeException) を投げる。ただし、このチェックは同期化されていないので古いデータを参照する危険性がある。
並行コレクション
同期化コレクションはステートへのアクセスを全て直列化しているのに対し、並行コレクションは複数のスレッドから並行アクセスされるように設計されているのでスループットが良い。
- ConcurrentHashMap
ロックの細分化が行われたHashMap
弱い整合性型のイテレータを返す(例外を投げての中断がない)。イテレータを作成する時点でのコレクションのステートのコピーを返すのでデータにアクセスする際には既に古いデータの可能性もあるが、並行処理の環境ではデータは常に変化するものなので完全に常に正しい値が必要な状況以外に対してはよいトレードオフがされている。
アトミックな操作を提供。その代わりにクライアントサイドロックによる機能追加が不可となっている。
- CopyOnWriteArrayList・CopyOnWriteArraySet
書き込み時に内部に保持している補助配列のコピーを作成し、そのコピーした配列のデータを変更した後にその配列を補助配列へと入れ替えるコレクション
イテレータはその時点での補助配列の参照を返し、そのイテレータが参照している配列自体は決して変わらないようになっている。
変更よりもイテレータを多用する際に適している。
- BlockingQueue
キューが満杯ならputをブロック、空ならtakeをブロックするキュー
時間制限付きのoffer・pollもあり。
サイズを無制限とする事も可能だが、状況によってはリソースが枯渇するので注意が必要。
プロデューサ・コンシューマパターンなどに使用される。
- プロデューサ・コンシューマパターン
- キューにタスクを入れる側(プロデューサ)とキューのタスクを取り出し実行する側(コンシューマ)に分離して考えるパターン
- シリアルスレッド拘束
- 可変オブジェクトの所有権を直列的に移転すること。直列的に移転することにより安全に所有権が移転できる。プロデューサ・コンシューマパターンなどに利用されている。
-
- 実装クラス
- LinkedBlockingQueue・ArrayBlockingQueue
- FIFOキュー
- PriorityBlockingQueue
- 優先度順のキュー
- SynchronousQueue
- タスクを保存するのではなく、スレッドのリストを保持して依頼するスレッドから実行するスレッドへタスクを直接渡すキュー
- Deque・BlockingDeque
先頭と末尾の両方から削除・追加ができる両頭キュー。
実装クラスは ArrayDeque・LinkedBlockingDeque がある。
ワークスティーリングやワークシェアリングなどに使用される。
ブロックとインタラプト
インタラプトとは強制的なスレッドの停止ではなく、適切なタイミングで仕事を中断出来るようにする為の仕組み。
- 基本的なインタラプトへの応答
- InterruptedExceptionを広める。そのままthrowするか、catchして適切な処理後にthrowする。
- インタラプトを再生する。Runnableの中などではInterruptedExceptionを投げられないので、catchして現在のスレッド上にinterruptを呼び出して復元する。
決して何もせずに握りつぶしてはいけない!!!
シンクロナイザ
自分のステートを使ってスレッドのコントロールフローを調停するオブジェクトのこと
- ラッチ
最終ステートに達するまでスレッドの前進を遅らせる機構のこと
-
- CountDownLatch
カウンターが0になるまでawait()メソッドを呼び出したスレッドをブロックする。一度0になると以降はブロックしない。
- FutureTask
終了ステートになるまでget()をブロックし、終了ステートになれば値を返す。返す値はコンストラクタに渡すCallableのcall()メソッドなどで指定する事が可能。
複数のスレッドを同期させたり制御したりする機構のこと
-
- 計数セマフォ
リソースにアクセスできる数や何らかの処理を実行できる数などを制御する機構のこと
-
- Semaphore
コンストラクタで許可の数を設定し、許可が0でなければ許可をもらい処理を実行し完了後に許可を返す。許可が0の場合は他のスレッドが許可を返すまでブロックされる。
許可数が1のセマフォを「二項セマフォ」と呼び、ミューテックスとしても利用可能。
- バリヤ
一定数のスレッドが揃うまでブロックして同期させる機構のこと
-
- CyclicBarrier
バリヤポイント(待機する場所)でawait()を呼び出し、一定数のスレッドがawait()するまでブロックするようになっている。
通過する為に必要なスレッドの数はコンストラクタにて設定する。
バリヤアクションと呼ばれるブロックから解放される前に実行する処理を設定することも可能。
-
- Exchange
2つのスレッドがバリヤポイントにてデータを交換するバリヤ
第1部 まとめ
- 可変ステートへのアクセスは正しく調停する。
- 可変ステートが少ないほどスレッドセーフの確保が容易である
- 可変にする必要のないフィールドはfinalにする
- 不可変オブジェクトは自動的にスレッドセーフとなる
- カプセル化は複雑性を十分に管理可能にしてくれる
- 可変ステートはすべてロックでガードする
- 1つの不変項を構成する全ての変数をガードする
- 複合アクションは最初から最後まで途切れずにガードする
- スレッドセーフ性は設計の段階からする
- 同期化ポリシーをドキュメントする(スレッドセーフでないのならスレッドセーフでないことも)
とりあえず一区切り。というか疲れたました…。
書いてて怪しいところや説明が足りないと思うところは所々あったんですけど調べてると時間がかかり過ぎますし、手元に本がない状態ですので確認が出来ず、間違い、抜け等々あるかもしれません。ツッコミ大歓迎ですので穴があったら突っ込んで下さい!
*1:この章はメモからそのまま写したら書いてて微妙になったので後で調べたり復刊されたら修正する予定