「増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編」第五章『Producer-Consumer』読了

読了.補講 1 の InterruptedException が意味するところや,wait, sleep, join の違いはしっかり覚えておきたい.

InterruptedException の意味合い

プログラムの中で InterruptedException が投げられる可能性があるメソッドには以下のような特徴がある.

・「時間がかかる」メソッドである.
・「キャンセルできる」メソッドである.
一言で言えば,throws InterruptedException がついためそっどは,時間はかかるかもしれないが,キャンセルできるメソッドなのです.
(179 ページより引用)

なかでも,標準クラスライブラリ中で throws InterruptedException が付いているメソッドは,
・wait (notify/notifyAll されるまで待つ,notify/notifyAll で待つのをキャンセル)
・sleep (引数で指定された時間待つ.指定時間が経つとキャンセル)
・join (指定したスレッドの終了を待つ.別スレッドの終了を待つのをキャンセル)
だそうである.

interrupt メソッドにおける sleep メソッドと wait メソッドの扱いの違い

sleep メソッドを途中でキャンセルする場合には interrupt メソッドを使用する.このメソッドを使用すると InterruptedException が投げられる.その際, Thread のインスタンスメソッドとして interrupt メソッドを使うのだが,そのインスタンスのロックを取る必要はない,どのスレッドでも自由に interrupt メソッドを呼び出すことができる.

一方,wait メソッドでは wait メソッドを使用した際にスレッドは一度ロックを開放しウェイトセットに入っているので,再びロックを取り直してからでないと投げることができない.

interrupt メソッドの機能

interrupt メソッドは直接 InterruptedException を投げているわけではない.このメソッドは,単に非インタラプト状態からインタラプト状態に変更するだけのインスタンスメソッドである.仮に interrupt メソッドが呼び出されたあとに通常の処理が書かれていた場合,その時点で中断するわけではなくそのまま処理を続行する.その後 sleep, wait, join メソッドなどが呼び出されるか,明示的に InterruptedException を投げるかしない限り,例外処理は行われない.

「増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編」第五章『Producer-Consumer』

第五章は『Producer-Consumer』と題されている.生産者 (Producer) と消費者 (Consumer) 2 種類のスレッド群がやり取りするようなプログラムを実現するためのパターンである.ただし両者の間に仲介者 (Channel) が存在し,そのやり取りの助けをする.

パティシエとお客さんとテーブル

この三者の関係を説明するために非常にわかりやすいサンプルプログラムが掲載されている.

登場人物はまず Producer はケーキを作るパティシエ (MakerThread).次に Consumer はケーキを食べるお客さん (EaterThread).そして Channel 役としてテーブル (Table) である.MakerThread はケーキ (ここでは String 型の cake) を作成し table に置く.置かれた cake を EaterThread がとり,それを食べる (cake を表示する) というプログラムである.table には規定のサイズ (ここでは一定の要素数を持った配列で表現している) があり,一定数以上の cake を置くことはできない.MakerThread, EaterThread ともに GuardedSuspension パターンが実装されており,MakerThread は限界以上の cake を table には置くことが出来ず,EaterThread は table に cake がない場合には食べることができない.

正に table を介して MakerThread と EaterThread は各自の処理を並行して行なっている.

仲介者を挟むメリット

Channel 役である table を介さずに MakerThread と EaterThread が直接 cake をやり取りすればいいようにも思える.しかし,Channel 役を挟むことで以下の2つのメリットがある.

処理の余裕

もし EaterThread と MakerThread が直接 cake をやり取りするとする.すると MakerThread は cake を作って EaterThread に渡し,そのまま EaterThread が食べ終わる (処理が終わる) のを待たなくてはならない.なぜなら両者の処理は synchronized で排他処理が行われているから.これは無駄な時間ロスである.

一方 table を介した場合,MakerThread はガード条件が満たされている限り cake を作り続け,その処理は EaterThread の処理に依存することはなく,どんどん cake を作り出すことができる.このように,仲介者が間にいることで Producer と Consumer の処理を独立させ,処理の効率性を上げることができる.

再利用性

synchronized, wait, notifyAll などを使用するのをすべて Channel に任せ,Producer と Consumer はマルチスレッドを意識せずとも処理を行うことができる.その結果,再利用性があがる.

スローガン

スレッドの協調動作では「間に入るもの」を考えよう
スレッドの排他制御では「守るべきもの」を考えよう

「増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編」第四章『Balking』読了

第四章『Balking』パターンを読み終わった.ガード条件が満たされていない場合には処理を途中でやめる,一定時間たっても処理が行われなければ中断する,などようやくマルチスレッドプログラミングっぽくなってきた.

java.util.concurrent.LinkedBlockingQueue

タイムアウト処理を行う場合,wait () を使って処理を待ち,System.currentTimeMills () で取得した経過時間による条件分岐によって,例外処理を throw するなどして実現していた.

しかし java.util.concurrent.LinkedBlockingQueue を使えばもっと手軽にタイムアウト処理を行うことができる.
(158~159 ページ,507~508 ページ)

例えば,キューから要素を取り出す poll メソッドの場合,以下のような使い方ができる.

result = queue.poll ( 30L, TimeUnit.SECONDS );

こう書くと,30 秒以内に要素が得られなかった場合には result に null を返す.

また,キューを追加する offer メソッドでは,

result = queue.offer ( request, 30L, TimeUnit.SECONDS );

とかくと,30 秒以内に要素が追加できなければ false を返す.

一つのスレッドを何度も start () すると

これは1度だけ実行できる.

start メソッドが再度呼び出された場合には,スレッドの起動が二度と起こらないように balk して,例外 IllegalThreadStateException を投げます
(508 ページから引用)

つまり Thread クラスの start メソッドでも balking パターンが使用されている.

ウェイトセットとエントリセット

第四章では Balking パターンについて解説されている.中でもここでは,一定時間過ぎれば処理を待たずに中断するというタイムアウトパターンについて学んでいる,

処理待ちという観点で言うと,スレッドには以下の 2 状態が存在する.

(1) synchronized でロックを取ろうとしてブロックしている状態
(2) wait を実行してウェイトセットの中にいる状態
(155 ページより引用)

この 2 状態はスレッドが活動していない点では似ているが,大きく異なる部分もある.

synchronized でロックを取ろうとしてブロックしている状態

(1) の状態にあるスレッドをタイムアウトさせる方法はありません.これは,synchronized メソッドにも synchronized ブロックにも,タイムアウトの値を指定する方法が無いことからもわかります.
(155 ページより引用)

とあるように,この状態からタイムアウトさせることはできない.

また,この状態にいるスレッドを「エントリセット (entry set) にいると表現し,ウェイトセットと対比させて説明されている.

wait を実行してウェイトセットの中にいる状態

これをタイムアウトさせるには,一定時間経過後に TimeoutException を投げるようなプログラムにすればいい.これは 153~154 ページで使用しているサンプルプログラムで説明されている.
また interrupt を実行すると Interrupted Exception が投げられるので,それを使用するのもよし.

Tomcat を Eclipse に導入

Java サーブレットの勉強をする必要があるので Tomcateclipse に導入.

本来は emacsソースコードを書いているので最初はそちらに導入しようとしたのだが,なかなかうまくいかなくて諦めた.こんなところで強情はってもしょうがない.とりあえず eclipse に入れてから考えよう.

導入手順は以下を参照.
渋谷生活 / Tomcat + Eclipse on Mac

簡単に流れを言うと,

1. eclipse 導入
2. MySQL 導入
3. Tomcat 導入
4. eclipseTomcat プラグイン導入

という感じ.1~3 はすでに済んでいたので比較的簡単に導入できた.

Balking パターン

第四章に突入.この章では Balking パターンについて学ぶ,

第三章で学んだ Guarded Suspension パターンではスレッドのガード条件が満たされていなかった場合は,ガード条件が満たされるまで待ち続けていた.Balking パターンでもガード条件が登場するのは同様だが,Guarded Suspension パターンとは異なり,ガード条件が満たされない場合にはすぐに処理を中断する.

この章で作成するサンプルプログラムでは,簡単なデータ自動保存プログラムのようなものである.データを変更し,しばらく間をおいた後保存を行うスレッドと,定期的にデータの変更を見に行き,変更されていた場合には保存を行うスレッドとの動きを見る.このプログラムでのガード条件は「データが変更されたか」であり,ガード条件が満たされていなかった場合には各スレッドは処理を balk する.

ようやく実用的なプログラムに使えそうな技術が出てきてテンションが上がってきた.この調子で今月末までにひと通りこの本の内容を終わらせたい.

「増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編」第三章『Guarded Suspension』読了

ちょっと間があいたが読み終わった.

ガード条件は if ではなく while

演習問題 3-4 (133 ページ) より.この章で扱っているサンプルプログラムでは,キューにリクエストを追加するスレッドとキュからリクエストをとりだし処理を行うスレッドの 2 種類が,ガード条件によって正常に処理を繰り返すことを目的としている.ここで,リクエストを取り出し処理を行う方のスレッドでは以下のようなガード条件が課されている.

public synchronized Request getRequest () {
    try {
	while ( queue.peek() == null ) {
		wait();
	    }
	} catch ( InterruptedException e ) {
    }
    return queue.remove();
}
(117 ページ List.3.2 より)

ここでは,queue.peek () == null がガード条件となっており,キューの中にリクエストがひとつもない場合は wait () でウェイトセットに入り,notifyAll がされるまで queue.remove() によるキューからのリクエスト取り出し処理を待ち続けるというものである.

では,このプログラム内でのガード条件部分を while から if に書き直すとどうか?

もし,複数のインスタンスがウェイトセットに入っていた場合,notifyAll によってすべてのインスタンスの待ち状態が解除される.その際にキューにリクエストが 1 個しかなかったならば,2 つ目以降のインスタンスは空っぽにもかかわらず queue.remove () を行うため,エラーが起こってしまう.

ここでは notifyAll がされるたびにガード条件を確かめ,本当にキューの中にリクエストがあるのかどうかを確認しなければならない,そのため,ガード条件の確認には if 文ではなく while 文を使用しなければならない.

wait と sleep の違い

演習問題 3-4 (133 ページ) より.イマイチ違いがわかっていなかった.両者の違いは,「ロックの解放」にある.

wait を実行したスレッドは,対象になっているインスタンスのロックを解放します.一方,Thread.sleep はインスタンスのロックを解放しません.
(495 ページより)

上記のプログラム内の wait(); を Thread.sleep (100); に変更したとする.すると,インスタンスのロックはかかったままなので定期的に目を覚ますが,何の処理も進まないプログラムになってしまう.

動いてはいるけれども実質的にまったく先に進まない状況のことを,一般にライブロック (livelock) と呼びます.
(495 ページより)