#Single Threaded Executionパターン
同時に一つのスレッドだけしか処理を実行できないように制限を設けるパターン。Single Threaded Executionパターンは、Critical Section、あるいはCritical Regionと呼ばれる場合もある。
(意図した通りに動くコードとそうでないコードが本書に記載されている)
##登場人物
Single Threaded Executionパターンには、**SharedResourece(共有資源)**という役割を持ったクラスが登場する。SharedResourece役はいくつかのメソッドを持つが、それは以下の2種類に分類される。
- safeMethod:複数のスレッドから同時に呼び出しても、もともと問題のないメソッド
- unsafeMethod:複数のスレッドから同時に呼び出されては困るので、ガードしなければならないメソッド
Single Threaded Executionパターンでは、unsafeMethadを、同時に一つのスレッドからしかアクセスできないようにガードする。Javaではsynchronizedメソッドにすることでガードを実現する。シングルスレッドで動作させる必要のある範囲をクリティカルセクションと呼ぶ。
##どんなときに使うのか
- 複数のスレッドからアクセスされるとき。SharedResource役のインスタンスが、複数のスレッドから同時にアクセスされる可能性があるとき。たとえマルチスレッドであっても、すべてのスレッドが完全に独立して動作しているなら不要。このような状態を、スレッドが干渉していない、という。
- SharedResource役の状態が変化する可能性があるとき。状態が変化する可能性が全くないのなら、Single Thread Executionパターンを使う必要なない。Immutableパターン(後述)がこれに相当する。
- 安全性を保つ必要があるとき。Javaのコレクションクラスの多くは、スレッドセーフになっていない。これは安全性を守る必要がない場合に高速に動作できるようにするため。スレッドセーフでないクラスは、APIリファレンスに明記されている。
ちなみに、Javaには、コレクションクラスをスレッドセーフにするためのメソッドが用意されている。
- synchronizedCollectionメソッド
- synchronizedListメソッド
- synchronizedMapメソッド
- synchronizedSetメソッド
- synchronizedSortedMapメソッド
- synchronizedSoretedSetメソッド
##生存性とデッドロック
ここは非常に重要なので丁寧に書いておく。
Single Threaded Executionにおけるデッドロックは、次の全ての条件を満たす時に起きる。
- 複数のSharedResource役がある。
- スレッドが、あるSharedResource役のロックを取ったまま、他のSharedResource役のロックを取りに行く。
- SharedResource役のロックを取る順序が定まっていない。
例えば、AとBの二人が目の前のごはんを食べるとする。食べるにはスプーンとフォークの両方が必要。Aがスプーンを取り、Bがフォークを取ったとする。このとき、AはBがフォークを置くのを待つし、BはAがスプーンを置くのを待つ。デッドロックが発生する。この例では
- スプーンとフォークという複数のSharedResource役が存在し、
- スプーンを握ったまま相手のフォークを取りに行こうとするのはロックを取ったまま他のロックを取りに行こうとすることだし、
- 今回はスプーン→フォークの順でもフォーク→スプーンの順でもよいので、ロックをとる順番が決まっていないことになる。
よってデッドロックが発生している。この例で条件1or2or3のいずれかを崩してみる。
1.を崩してみる。食べるにはスプーンだけでよいとする。これだとデッドロックは発生しない。
2.を崩してみる。スプーンを握ったままフォークを取りに行くのをやめる。スプーンで食べれるものはスプーンで食べて、食べ終わったらスプーンを一度置いて、次フォークを取りに行く。これでもデッドロックは発生しない。
3.を崩してみる。必ずスプーンとフォークという順番で取らないと行けないと決めておく。そうすると、Aがスプーンを取ると、Bはスプーンが置かれるまで待たなければならないので(Bはフォークを取ることはないので)、Aはフォークを取れる。デッドロックは発生しない。
##再利用性と継承異常
SharedResource役をきっちり作って安全性を確保しても、サブクラス化によって安全性が崩されてしまう危険がある。マルチスレッドプログラミングにおいて、継承がやっかいな問題を引き起こすことがある。これを継承異常と呼ぶ。
##クリティカルセクションの大きさとパフォーマンス
一般に、Single Threaded Executionパターンは、パフォーマンスを低下させる。
- 理由1:ロックの取得に時間がかかるため
- 理由2:スレッドの衝突によって待たされるため
##synchronizedについて考える
例えば以下のコードはsynchronizedを使ったコードと同じか。
void method() {
lock(); // 仮にこういうメソッドがあったとする
...
unlock(); // 仮にこういうメソッドがあったとする
}
答えは、同じではない。なぜなら、lock(), unlock()の間でreturnや例外処理が走った場合、ロックが解放されない。これに対してsynchronizedメソッドやsynchronizedブロックは、{}を抜けるとロック解放をするので、途中でreturnしようが例外を投げようが、確実にロックを解放してくれる。もし、lock()とunlock()のような対になるメソッドがあって、何が起ころうともunlock()を呼びたい場合はfinallyを使う。
void method
lock();
try {
...
} finally {
unlock(); // finallyにunlockを入れる
}
}
このようなfinallyの使い方は、Before/Afterパターンの実現方法の一つ。
synchronizedメソッド、synchronizedブロックは以下に気をつけること。
- 何を守っているか。
- 他の場所でもそれを守っているか
- どの単位で守るか
- どのインスタンスのロックを取るのか
##アトミックな操作
基本型(primitive type)と参照型(reference type)の代入や参照はアトミック。つまり、あるスレッドがn=123という代入を行い、別のスレッドがn=456という代入を行った場合、nは必ず123か456になる。しかし、Javaの言語仕様上は、longとdoubleの代入や参照はアトミックでない。実際には多くのJava言語処理系でlongもdoubleもアトミックな操作として実装されているだろう。volatileというキーワードをつけると、そのフィールドへの操作はアトミックになる。
##計数セマフォ
Single Threaded Executionはある領域を「たった一つのスレッド」だけが実行するパターン。ある領域を「最大N個のスレッド」まで実行できるようにするのが計数セマフォ。java.util.concurrentパッケージには、計数セマフォを表すSemaphoreクラスが提供されている。Semaphoreのコンストラクタでリソースの数(permits)を指定。Semaphoreのacquireメソッドでリソースを確保、releaseメソッドでリリースを解放する。acquireとreleaseは必ず対になって呼ぶ必要がある。そのためfinallyを使って前述のBefore/Afterパターンを作るとよい。
関連
『Java言語で学ぶデザインパターン(マルチスレッド編)』まとめ(その1)
『Java言語で学ぶデザインパターン(マルチスレッド編)』まとめ(その2)