デザインパターン入門 マルチスレッド編まとめ

がーっと読んだ。実際に使うときの思い出しトリガーになるようにメモしておく。

Java言語で学ぶデザインパターン入門 マルチスレッド編

マルチスレッドプログラムの評価基準

  • 安全性 オブジェクトを壊さないこと
    • スレッドセーフなクラス
  • 生存性 必要な処理が行われること
    • 安全性を重視しただけでは生存性を下げてしまう場合がある。例えばデッドロック
  • 再利用性 クラスを再利用できること
    • スレッドの排他制御の仕組みや方針をうまくクラスの中に隠蔽すれば、再利用性の高いプログラムになる。
  • パフォーマンス 処理を高速・大量に行えること

安全性と生存性を守るのは必須。その上で、いかにして再利用性とパフォーマンスを上げるかが重要。

Single Thread Execution 「この橋を渡れるのは、たった一人」

Immutable 「壊したくとも、壊せない」

  • インスタンス生成後、状態が変化しないのであればImmutableパターンにすることを検討する
  • Immutableなオブジェクトにすると排他制御に必要がなくなることが大きなメリット
  • Immutableなクラスの設計には十分に注意を払うこと。特にプリミティブでないオブジェクトをメンバに持つ場合には。

Guarded Suspension 「用意できるまで、待っててね」

  • これも頻出パターン
  • インスタンスが適切な状態になるまで、ガード条件を使ってスレッドを待たせる。
  • ガード条件が変化したことをスレッドの通知(notifyAll)
  • ガード条件が満たされないなら待たずに帰る場合は、Balkingパターンを使う。

Balking 「必要なかったら、やめちゃおう」

  • Guarded Suspensionパターンで、ガード条件が満たされない場合に待たずに帰る

Producer-Consumer 「わたしが作り、あなたが使う」

  • あるスレッド(Producer)から別スレッド(Consumer)にデータを安全に渡したい。
  • ProducerとConsumerの処理スピードが違うとスループットが落ちる。
  • 中継地点にChannelを置く。Channelに複数個のデータを保持させ、処理スピードの違いを吸収する(スループット向上)

Read-Write Lock 「みんなで読んでもいいけれど、読んでる間は書いちゃダメ」

  • 複数のスレッドがインスタンスを共有、参照するだけのスレッド(Reader)と更新するスレッド(Writer)が存在
  • スレッドの排他制御をしないと安全性を確保できない。Single Thread Executionを使うとスループットが落ちる
  • まず、「Readerを制御するロック」と「Writerを制御するロック」を分ける
  • この2種類のロックを提供するReadWriteLockを導入する。
  • ReaderとWriter、Writer同士の排他制御は行い、Reader同士の排他制御は行わない(スループット向上)

Thread-Per-Message 「この仕事やっといてね」

  • スレッド(Client)がインスタンス(Host)のメソッドを呼び出している場合、メソッド処理が終了するまでHostから制御が戻ってこない。(応答性ダウン)
  • Hostが処理用のスレッドを新たに起動し、処理を任せる。Clientにはすぐに応答する。(メソッドの起動とメソッドの実行の分離)
  • 処理結果を戻したい場合は、Futureパターンを使う。
  • スレッドを使いまわしたい場合は、Worker Threadパターンを使う。

Worker Thread 「仕事が来るまで待ち、仕事が来たら働く」

  • Thread-Per-Messageパターンで、処理するスレッドをあらかじめ起動しておき、使いまわす。
  • リクエストを表現するインスタンスをWorkerスレッドに渡すときには、Producer-Consumerパターンを使う。

Future 「引換券をお先にどうぞ」

  • Thread-Per-MessageパターンやWorker Threadパターンで処理結果を得たい場合に使う。
  • Clientはリクエスト発行後、すぐにFutureインスタンスを受け取る
  • 処理終了後、Futureインスタンスに処理結果をセットする
  • ClientはFutureにセットされた処理結果を受け取る

Two-Phase Termination 「後片付けしてから、おやすみなさい」

  • 動作しているスレッドを安全に終了させたいときに利用できるパターン
  • 終了処理を行うタイミングは終了するスレッド本人に判断させる
  • スレッドを終了させるために「終了要求」用のメソッドを用意
  • 終了要求を受けたスレッドは、安全に終了処理を実行できるタイミングで終了処理を開始する

Thread-Specific Storage 「スレッドごとのコインロッカー」

  • シングルスレッドの環境で動作することを想定しているオブジェクトを、マルチスレッド環境で利用したいときに使うパターン。
  • スレッドごとにデータを格納したい場合に使うパターン
  • JavaではThreadLocalクラスで実現できる

Active Object 「非同期メッセージを受け取る、能動的なオブジェクト」

  • シングルスレッドで動作することを前提として設計されているServant役に皮をかぶせて、マルチスレッドのClient役から利用できるようにするパターン。
  • Servantを呼び出すScheduler役のスレッドを導入。Workerスレッドが1個のWorker Threadパターン。これで複数のClientが処理できる。
  • Clientからの要求はProxyへのメソッド呼び出しとして実現。Proxyで要求を1つのオブジェクトに変換し、Producer-Consumerパターンを使ってSchedulerに渡す。(Clientへの応答性の確保)
  • 実行すべき要求の選出はSchedulerが行う。
  • 実行結果はFutureパターンを使ってClientへ返す。

【付録】Javaのメモリモデル

  • Javaのメモリモデルには、「メインメモリ」と「ワーキングメモリ」がある。
    • メインメモリはインスタンスが存在する領域。個々のスレッドはメインメモリには直接アクセスできない。
    • ワーキングメモリは個々のスレッドが持っている作業領域。必要に応じて、メインメモリからワーキングメモリ(またはその逆)にコピーが行われる。
  • 各スレッドは常にメインメモリにある最新の状態を参照できるとは限らない。(仕様上)
  • synchronizedの2つの働き
    • スレッドの同期
    • メモリの同期。ワーキングメモリとメインメモリの内容の同期が取られる。(ただしsynchonizedに入るときのみ)
  • volatileの2つの働き
    • メモリの同期
    • long、doubleの代入をアトミックに行う。


Java言語で学ぶデザインパターン入門 マルチスレッド編