目次| |



17. スレッド及びロック

これまでの記述の大半は,文又は式を,一度に一つ,つまり一つの スレッド(thread) で実行するJavaコードの振舞いだけに関係していたが,Java仮想計算機は,一度に多数のスレッドを実行することができる。 これらのスレッドは,共有主メモリに存在するJavaの値及びオブジェクトを操作するJavaコードを独立して実行する。 複数のスレッドは,複数のハードウェアプロセッサを搭載したり,一つのハードウェアプロセッサを時分割したり,又は複数のハードウェアプロセッサを時分割することによって,実現してもよい。

Javaは,スレッドの並行活動を 同期化する(synchronizing) 機構を提供することによって,並行的だが決定論的な振舞いを示すプログラムを支援する。 スレッドを同期化するために,Javaでは,モニタ(monitor) を使用する。 モニタは,モニタが保護するコード領域を一度にただ一つのスレッドだけが実行可能にするための高度な機構とする。 モニタの振舞いは,ロック(lock) によって表現する。各オブジェクトには,一つのロックが存在する。

synchronized 文(14.17)は,マルチスレッド操作にだけ関係する次の二つの特別な動作を実行する。

便宜上,メソッドを synchronized 宣言してよい。このようなメソッドは,その本体が synchronized 文に含まれているかのように動作する。

クラス Object のメソッド wait(20.1.620.1.720.1.8), notify(20.1.9),及び notifyAll(20.1.10)は,一つのスレッドから他のスレッドへの効率的な制御の転送を提供する。スレッドは,計算に手間のかかる,単なる“スピン”(内部状態の変化の有無を知るために,オブジェクトのロック設定とロック解除を繰り返すこと)ではなく,むしろ他のスレッドが notify を使って目覚めさせるときまで wait でそれ自体を一時停止させることができる。これは,スレッドが(共通の資源を共有するとき競合しないようにする)相互排他関係よりもむしろ,(共通の目標について積極的に協力する)生産者−消費者関係をもつ状況に,特に適している。

スレッドは,コードを実行する際に一連の動作を実行する。スレッドは,変数の値を 使用(use) したり,又は変数に新しい値を 代入(assign) してよい。(その他の動作として,算術演算,条件検査,及びメソッド呼出しを含むが,これらは,直接には変数に関係しない。) 複数の並行スレッドが,一つの共有変数に関して動作すれば,その変数への動作がタイミング依存な結果となることがある。 このタイミング依存性は,並行プログラミング固有のもので,Javaにおいて,この規格だけではプログラムの実行結果が決定できない数少ない部分の一つとなる。

各スレッドは,作業メモリをもち,その中に,すべてのスレッドが共有する主メモリ上の変数値の複製を保持してもよい。共有変数にアクセスするためには,スレッドは,通常まずロックを取得し,その作業メモリをフラッシュする。これは,共有値が共有主メモリからスレッドの作業メモリにロードされることを保証する。 スレッドがロックを解除するとき,作業メモリに保持された値が主メモリに書き込まれることを保証する。

ここでは,特定の低水準の動作に関する,スレッドの主メモリとの相互作用,及びスレッド同志の相互作用について規定する。 これらの動作が発生してもよい順序についての規則が存在する。 これらの規則は,Javaの任意の実装への制約を与え,Javaプログラマは,規則に依存することで並行 Javaプログラムの可能な動作を予測できる。 しかしながら,規則では,意図的に開発者へ自由裁量を与えている。その意図は,並行コードの実行速度及び効率の大幅向上を可能とする標準的なハードウェア及びソフトウェア技術を受け入れるためである。

規則の要点を次に示す。

17.1 用語及び枠組み

変数(variable) は,値が記憶できるJavaプログラム内の任意の場所とする。変数には,クラス変数及びインスタンス変数だけでなく,配列の構成要素も含む。変数は,すべてのスレッドで共有される 主メモリ(main memory) に保持される。あるスレッドは,他のスレッドの仮引数又はローカル変数にアクセス不可能なので,仮引数及び局所変数は,共有主メモリにあると考えても,又はそれらを所有するスレッドの作業メモリにあると考えてもよい。

すべてのスレッドは,作業メモリ(working memory) をもつ。そこには,使用又は代入しなければならない変数の個別の 作業コピー(working copy) を保持する。スレッドがJavaプログラムを実行する際は,この作業コピーに対して操作をする。主メモリは,すべての変数のマスタコピー(master copy)を含む。スレッドが変数の作業コピーの内容を,マスタコピーに転送する又はその逆を行うことをいつ許すか、又はいつ要求するかについての規則が存在する。

主メモリには,ロック も含まれる。それぞれのオブジェクトに関連した一つのロックが存在する。スレッドは,ロックを取得するために競合してもよい。

ここでは,動詞,使用代入ロード(load)記憶(store)ロック設定,及び ロック解除 を,スレッドが実行できる 動作(actions) を指すものとして使用する。 動詞,読取り(read)書込み(write)ロック設定,及び ロック解除 は,主メモリサブシステムが実行できる動作を指すものとする。これらの各動作は,アトム的(atomic,分割不能) とする。

使用 又は 代入 動作は,スレッドの実行エンジン及びスレッドの作業メモリ間の緊密な相互作用とする。ロック設定 又は ロック解除 動作は,スレッドの実行エンジン及び主メモリの緊密な相互作用とする。 しかし,主メモリ及び作業メモリの間のデータ転送は,緊密ではない。 データを主メモリから作業メモリに複写するときには必ず,主メモリで実行される 読取り 動作,に引き続いてある時間後に,作業メモリで実行される対応する ロード 動作,の二つの動作が発生しなければならない。 データを作業メモリから主メモリに複写する際も,作業メモリで実行される 記憶 動作,に引き続いてある時間後に,主メモリで実行される対応する 書込み 動作,の二つの動作が発生しなければならない。 主メモリ及び作業メモリの間には,遷移時間(transit time)が存在してもよく,遷移時間は,トランザクションごとに異なってもよい。 つまり,あるスレッドによって開始される異なる変数に対する動作は,他のスレッドからは,異なる順序に発生するように見えてもよい。 しかしながら,個々の変数に対しては,任意の一つのスレッドについての主メモリの動作は,そのスレッドの対応する動作と同じ順序で実行される。 (これは,次の規定によってより詳細に規定される。)

一つのJavaスレッドは,実行しているJavaプログラムの意味規則によって指示された,使用代入ロック設定,及び ロック解除 の一連の動作を発行する。 その下のレベルのJava処理系では,次に説明する制約に従うために,さらに,適切な ロード記憶読取り,及び 書込み 動作を実行することが要求される。 Javaの処理系がこの規則に正しく従い,Javaの応用プログラマが特定の他のプログラミング規則に従っていれば,データは,共有変数を介してスレッド間で信頼できるように転送される。 規則は,これを可能にするように十分“緊密”に設計されており,かつ,ハードウェア及びソフトウェアの設計者が,レジスタ,キュー,及びキャッシュなどの機構を介して実行速度及びスループットを自由に改善できる程度にゆるく設計されている。

各動作の詳しい定義を,次に示す。

したがって,スレッドと変数の時間的な相互作用は,一連の使用代入ロード,及び 記憶 の並びからなる。主メモリは,すべての ロード に対して 読取り 動作を実行し,すべての 記憶 に対して 書込み 動作を実行する。スレッドとロックの時間的な相互作用は,一連のロック設定 及び ロック解除 動作の並びからなる。したがって,スレッドの大域的に見ることのできる振舞いは,変数及びロックに関するすべてのスレッド動作からなる。

17.2 実行順序

実行順序の規則は,特定のイベントが発生できる順序を制約する。動作間の関係は,次の四つの一般的な制約を受ける。

最後の規則は,些細なことのように思えるかもしれないが,完全を期すために,別項目として明確に述べる必要がある。 この規則が無ければ,複数のスレッドが一連の動作の集合を要求し,かつ要求された動作間の優先関係が,他の規則をすべて満たしているが,同じ動作を続けて行う必要がある,ようなことが可能となる。

スレッド同志は,直接には相互作用しない。共有主メモリを介してだけ通信する。

スレッドの動作及び主メモリの動作間の関係は,次の三つの制約を受ける。

次の規則の大半は,ある特定の動作の実行における順序をさらに制約する。 規則は,ある動作が他の動作の前又は後で行われねばならないことを規定している。この関係は,推移的とする。つまり,動作 A が動作 B に先行しなければならず,動作 B が動作 C に先行しなければならないならば,動作 A は,動作 C に先行しなければならない。 プログラマは,これらの規則が動作の順序 だけ(only) を制約することを覚えておかねばならない。 動作 A が動作 B に先行しなければならないことを,規則又は規則の組み合わせが,示さなければ,Java処理系は,自由に動作 B を動作 A の前に実行したり,又は動作 A と並行に実行してよい。 この自由裁量は,高性能の鍵となるが,処理系は,そのすべてを利用しなくともよい。

次の規則において,“B は,A 及び C の間に介入しなければならない”という表現は,“動作 B は,動作 A に続き,動作 C に先行する”という意味とする。

17.3 変数についての規則

T をスレッドとし,V を変数とする。V に関し,T によって実行される動作について,次の制約が存在する。

上記及び次のすべての制約に従うならば,ロード 又は 記憶 動作は,実装の都合に従って,任意の変数に対して任意のスレッドによって任意の時点で実行してよい。

主メモリ上で実行される 読取り 及び 書込み についても,次の制約が存在する。

この最後の規則は,スレッドによる 同じ(same) 変数への動作に だけ(only) 適用されるので注意すること。ただし,volatile 宣言された変数 (17.7)に対しては,これよりも厳密な規則が存在する。

17.4 double 及び long の非アトム的な取扱い

double 変数又は long 変数が volatile 宣言されていないとき,ロード記憶読取り,及び 書込み 動作の実行は,これらがそれぞれ32ビットの二つの変数であるかのように扱われる。これらの動作のいずれかが規則上要求されたときはいつでも,それぞれ32ビットの二回の動作として実行される。64ビットの double 変数又は long 変数を,二つの32ビット量として扱う方法は,実装依存とする。

これは,double 又は long 変数の 読取り 又は 書込み が,実際の主メモリによって二つの32ビットの 読取り 又は 書込み として処理されることによって,時間的に分離され,その間に他の動作が入り込むことがあるということに関連している。その結果,二つのスレッドが,共有した同じ非 volatile double 変数又は非 volatile long 変数に,異なる値を並行して代入した場合,その変数を後で使用したとき,いずれの代入値とも等しくない,実装に依存した二つの値の混合値が得られることがある。

double 及び long 変数の ロード記憶読取り,及び 書込み 動作を,処理系は,アトム的な64ビット動作として実装してもよい,実際には,これを強く推奨する。本モデルは,64ビット量への効率的なアトム的メモリトランザクションを提供できない現在のマイクロプロセッサのために,32ビットずつに分割している。Javaとしては,一つの変数について,すべてのメモリトランザクションをアトム的として定義した方が簡単である。この複雑な定義は,現在のハードウェア実装への現実的な譲歩である。将来には,この譲歩は,削除されるかもしれない。当分の間は,プログラマは,共有 double 変数,及び共有 long 変数へのアクセスは,常に明示的に同期化するように注意すること。

17.5 ロックについての規則

T をスレッドとし,L をロックとする。T によって L に対して実行される動作には,次の制約が存在する。

あるロックに関しては,すべてのスレッドによって実行される ロック設定 及び ロック解除 動作は,ある全体的に連続した順序で実行される。この全体的な順序は,各スレッドの動作の全体的な順序と矛盾してはならない。

17.6 ロック及び変数の相互作用についての規則

T を任意のスレッド,V を任意の変数,L を任意のロックとする。V 及び L についてT によって実行される動作には,次の制約が存在する。

17.7 Volatile宣言された変数の規則

変数が volatile 宣言されていれば,付加的な制約を各スレッドの動作に適用する。T をスレッド,V 及び Wvolatile 宣言された変数とする。

17.8 予測される記憶動作

変数が volatile 宣言されていなければ,これまでに説明した規則は,少し緩和され,記憶 動作を,これまでの規則が許すより前に行うことができる。この緩和の目的は,Javaコンパイラの最適化でコードの並替えを可能にするためとする。この時,適正に同期化されたプログラムの意味は,保存されるが,適正に同期化されていないプログラムでは,メモリ動作実行の順番が狂って実行されるかもしれない。

T による V への 記憶 が,T による V への特定の 代入 に続くと仮定する。これまでの規則に従って,T による V への ロード 又は 代入 が介在しないものとする。この場合,記憶 動作は,代入 動作がスレッド T の作業メモリに入れた値を主メモリに送る。次の制約に従う限り, 記憶 動作が 代入 動作の前に発生してもよい。

この最後の制約によって,事前の 記憶動作を 予測(prescient) と呼ぶ。予測では,本来,後続する 代入 によって記憶される値を,なんらかの方法で前もって知らなければならない。実際,最適化されたコンパイル済みコードは,このような値を前もって計算し(これは,例えば,計算が副作用を持たず,例外を投げないときにだけ許されている),前もって早めに記憶し(例えば,ループに入る前に),ループ内での後の使用のために作業レジスタに保持する。

17.9 議論

ロック及び変数の間の関連は,純粋に協定的なものとする。任意のロックにロック設定することは,概念上スレッドの作業メモリから すべて(all) の変数をフラッシュし,任意のロックを解除することは,概念上スレッドが代入した すべて(all) の変数を主メモリへ書込むことを強制する。つまり,ロックが特定のオブジェクト又はクラスと関連してもよいことは,純粋に協定的なものとする。アプリケーションによっては,例えば,いずれかのインスタンス変数にアクセスする前に,常にオブジェクトにロック設定するのが適切となるかも知れない。同期化メソッドは,この協定に従うための便利な方法とする。他のアプリケーションでは,一つのロックを使用して,オブジェクトの大きな集まりへのアクセスを同期化すれば十分かもしれない。

あるスレッドが特定の共有変数を,特定のロック設定後にだけ使用し,その同じロックの対応するロック解除前でだけ使用しているならば,そのスレッドは,ロック設定 動作後に主メモリからその変数の共有値を読み取り,ロック解除 動作前に,必要であれば,その変数に代入された最新の値を主メモリに複写することになる。この規則は,ロックへの相互排他規則を併用することによって,共有変数を介して,一つのスレッドから他のスレッドに値が正しく転送されることを保証している。

volatile 宣言された変数に関する規則は, volatile 宣言された変数の主メモリは,それぞれの 使用 及び 代入 ごとに,厳密に一度だけスレッドによって触られること,及びその主メモリは,そのスレッドの実行意味規則によって指示された順序で触られることを要求している。しかし,volatile 宣言されていない変数への 読取り 及び 書込み 動作に関しては,そのようなメモリ動作は,要求されていない。

17.10 例: 実行可能なスワップ

クラス変数 a 及び b,並びにメソッドhither 及び yonをもつクラスを考える。

class Sample {
int a = 1, b = 2;
void hither() {
a = b;
}
void yon() {
b = a;
}
}

ここで,二つのスレッドが生成され,一方のスレッドが hither を呼び出し,他方のスレッドが yon を呼び出すものとする。要求される動作の集合及び順序付けの制約を考察する。

hither を呼び出すスレッドを考える。規則に従うと,このスレッドは,b の 使用 を実行し,その後で a の 代入 を実行しなければならない。これを,メソッド hither への呼出しを実行するための最低条件とする。

ここで,そのスレッドによる変数 b についての最初の動作は,使用 ではあり得ない。それは,代入 又は ロード のはずである。そのプログラムテキストは,代入 動作を行っていないので,bへの 代入 は,起こり得ない。そこで,b の ロード が要求される。そのスレッドによるこの ロード 動作は,結果的に,主メモリによる b の先行する 読取り動作を要求する。

そのスレッドは,代入 の後に a の値をオプションで 記憶 してもよい。それを実行するならば,その 記憶動作は,結果的に,後続する主メモリによる a の書込み 動作を要求する。

yonを呼び出すスレッドへの制約も同じだが,aと b の役割は逆とする。

動作の全集号は,次のように記述できる。

ここで,動作 A から動作 B への矢印は,AB に先行しなければならないことを示す。

主メモリによる動作の発生順序を考察する。唯一の制約は,aの 書込み が a の 読取り に先行すること,及び,b の 書込み が b の 読取り に先行すること,の両方が不可能なこととなる。その理由は,上図の因果律を表す矢印がループ状を形成し,その結果,ある動作がそれ自体に先行することになるからであり,これは許されない。必須ではない 記憶 及び 書込み 動作が発生すると仮定すると,主メモリが規則に従って動作を実行する順序は,3通り存在する。ha 及び hb を hither スレッド用の a 及び b の作業コピーとし,ya 及び yb を yon スレッド用の作業コピーとし,ma 及び mb を主メモリ内のマスタコピーとする。初期値は,ma=1 及び mb=2 とする。この場合,3通りの動作の可能な順序及びその結果状態は,次の通りとする。

したがって,全体的な結果としては,主メモリ内で b が a に複写される,a が b に複写される,又は a 及び b の値が交換されるのいずれかとなる。さらに,変数の作業コピーは,一致する場合も一致しない場合もある。これらの結果のどれか一つが他の結果よりも適切と仮定することは正しくない。これは,Javaプログラムの振舞いが必然的にタイミング依存になる一例となる。

実装によっては,記憶 及び 書込み 動作の両方を実行しないようにしたり,そのいずれかだけを実行しないようにするかもしれない。この場合,実装によって可能な,また別の結果を生じる。

次に,この例を、synchronized メソッドを使用して修正したとする。


class SynchSample {
        int a = 1, b = 2;
        synchronized void hither() {
                a = b;
        }
        synchronized void yon() {
                b = a;
        }
}

もう一度,hither を呼び出すスレッドを考える。規則に従うと,このスレッドは,メソッド hither の本体を実行する前に(クラス SynchSample に対するクラスオブジェクトについての),ロック設定 動作を実行しなければならない。その後で b の 使用 及び a の 代入 動作が続く。最後に,メソッド hither の本体が終了した後で,クラスオブジェクトへの ロック解除 動作を実行しなければならない。これを,メソッド hither の呼出しを実行するために要求される最低条件とする。

前述のように,b の ロード が要求され,結果的に,この ロード が先行する主メモリによる b の 読取り 動作を要求する。ロック設定 動作の後に ロード が行われるので,対応する 読取りロック設定 動作の後でなければならない。

ロック解除 動作が a の 代入 に続くので,a への 記憶 動作は,必須とする。この 記憶 は,結果的に,後続する主メモリによる a の書込み 動作を要求する。その 書込み は,ロック解除 動作に先行しなければならない。

yon を呼び出すスレッドに関する状況も同じであるが,a 及び bの役割は逆とする。

動作の全集合は,次のように記述できる。

ロック設定 及び ロック解除 は,主メモリによる動作の順序に関してさらに制約を与える。あるスレッドによる ロック設定 動作が,他のスレッドの ロック設定 及び ロック解除 の間で発生することはできない。さらに,ロック解除 動作は,記憶 及び 書込み 動作が発生することを要求する。したがって,2通りの順序だけが可能となる。

結果の状態は,タイミングに依存するが,二つのスレッドで必ず a 及び b の値が一致することがわかる。

17.11 例: 無秩序書込み

この例は,一方のメソッドが両方の変数に代入し,他方のメソッドが両方の変数を読み取る点を除いて,17.10の例と同様とする。クラス変数 a 及び b,並びにメソッド to 及び fro をもつクラスを考える。


class Simple {
        int a = 1, b = 2;
        void to() {
                a = 3;
                b = 4;
        }
        void fro() {
                System.out.println("a= " + a + ", b=" + b);
        }
}

二つのスレッドが生成され,一方のスレッドが to を呼び出し,他方のスレッドが fro を呼び出すとする。要求される動作の集合及び順序付けの制約を考察する。

to を呼び出すスレッドを考える。規則に従うと,このスレッドは,b の 代入 の前に a の 代入 を実行しなければならない。これがメソッド to の呼出しを実行するための最低条件となる。同期化が行われていないので,代入値を主メモリに 記憶 するかどうかは,実装のオプションとする。したがって,fro を呼び出すスレッドは,a の値として 1 又は 3 を取得してよく,それとは独立に,b の値として 2 又は 4 を取得してよい。

次に,tosynchronized とし,fro は,そのままとする。


class SynchSimple {
        int a = 1, b = 2;
        synchronized void to() {
                a = 3;
                b = 4;
        }
        void fro() {
                System.out.println("a= " + a + ", b=" + b);
        }
}

この場合,メソッド to は,メソッドの最後の ロック解除 動作の前に代入値を主メモリに強制的に 記憶 する。当然,メソッド fro は,a 及び b を(この順序で)使用しなければならない。したがって,a 及び b の値を主メモリからロード しなければならない。

動作の全集合は,次のように記述される。

ここで,動作 A から動作 B への矢印は,AB に先行しなければならないことを示す。

主メモリによる動作の発生順序を考察する。規則は,a の 書込み は,b の 書込み の前に発生することを要求していないし,a の 読取り は, b の 読取り の前に発生することも要求していないことに注意すること。さらに,メソッド tosynchronized であっても,メソッド fro は, synchronized ではないため,ロック設定 及び ロック解除 の間の 読取り動作 を禁止するものはないことにも注意のこと。(重要な点は,一つのメソッドを synchronized 宣言しても,それだけではそのメソッドがアトム的であるかのように動作するわけではない。)

その結果として,メソッド fro は,a の値としてやはり 1 又は 3 を取得することがあり,それとは独立に b の値として 2 又は 4 を取得することがある。特に,fro では,a が 1 及び b が 4 になる場合もある。したがって,to が a への 代入 を行い,その後で b への 代入 を行ったとしても,その主メモリへの 書込み 動作は,他のスレッドからは,逆の順序で行われたかのように見えてもよい。

最後に,to 及び fro の両方を synchronized とする。


class SynchSynchSimple {
        int a = 1, b = 2;
        synchronized void to() {
                a = 3;
                b = 4;
        }
        synchronized void fro() {
                System.out.println("a= " + a + ", b=" + b);
        }
}

この場合,メソッド fro の動作は,メソッド to の動作の間に入ることができず,fro は,"a=1,b=2" 又は "a=3,b=4" を出力する。

17.12 スレッド

スレッドは,組込みクラス Thread(20.20)及びクラス ThreadGroup(20.21)によって,生成及び管理される。Threadオブジェクトを生成するとスレッドが生成されるが,これをスレッドを生成する唯一の方法とする。スレッドは,生成時点では,まだ活動的にはなっていない。スレッドは,メソッド start(20.20.14)が呼び出されると,実行を開始する。

すべてのスレッドは,優先度(priority) をもつ。処理資源に関して競合が存在するとき,一般には,優先度の高いスレッドが優先度の低いスレッドに優先して実行される。ただし,このような優先度は,最も高い優先度のスレッドが常に実行されることを保証するものではなく,高い信頼性で相互排他を実装するために,スレッドの優先度を使用することはできない。

17.13 ロック及び同期

すべてのオブジェクトには,ロックが存在する。Java言語では,ロック設定 及び ロック解除 を独立して実行することはできない。代わりに,そのような動作を常に正しく対になるように調整する高水準の構文によって,それらは,暗黙的に実行される。(ただし,Java仮想計算機では,ロック設定 及び ロック解除 動作を実装する,独立したmonitorenter 命令及び monitorexit命令を提供していることに注意すること。)

synchronized 文 (14.17)は,オブジェクトへの参照を計算する。その後で,そのオブジェクトへの ロック設定 動作の実行を試み,ロック設定 動作が正常完了するまで先の処理に進まない。(ロック設定 動作は,遅延してもよい。その理由は,ロックに関する規則は,ある他のスレッドが一つ以上のロック解除 動作を行う準備ができるまで,主メモリの参加を禁止することができるからである。)ロック設定 動作が実行された後で,synchronized 文の本体が実行される。本体の実行が,正常完了又は中途完了のいずれかで終了した場合,その同じロックへの ロック解除 動作が自動的に実行される。

synchronized メソッド(8.4.3.5)は,呼び出されたときに,自動的に ロック設定 動作を実行する。その本体は,ロック設定 動作が正常に完了するまで実行されない。メソッドがインスタンスメソッドならば,それに対して呼び出されたインスタンス(つまり,そのメソッドの本体の実行中に this として参照されるオブジェクト)に関連するロックにロックを設定する。メソッドが static ならば,メソッドが定義されたクラスを表すClassオブジェクトのロックにロックを設定する。本体の実行が,正常完了又は中途完了のいずれかで終了した場合,その同じロックへの ロック解除 動作が自動的に実行される。

ある変数が,ある一つのスレッドで代入され,他のスレッドで使用又は代入される場合,その変数に対するすべてのアクセスは,synchronizedメソッド又はsynchronized 文内に囲まれなければならない,というのが最良の方法となる。

Javaは,デッドロック状態を,防止しないし,検出を要求することもしない。スレッドが,複数のオブジェクトへのロックを(直接的又は間接的に)保持するプログラムは,必要ならば,デッドロックが生じない高水準のロックプリミティブを作成して,従来からあるデッドロック回避手法を使用しなければならない。

17.14 待機集合及び通知

すべてのオブジェクトは,関連するロックをもつことに加えて,関連する待機集合(wait set)をもつ,それは,スレッドの集合とする。オブジェクトが最初に生成されたときは,その待機集合は空とする。

待機集合は,クラス Object のメソッド wait(20.1.620.1.720.1.8),メソッド notify(20.1.9),及びメソッド notifyAll(20.1.10)が使用する。これらのメソッドは,スレッドのスケジューリング機構(20.20)とも相互作用する。

メソッド wait は,現在のスレッド(T と呼ぶ)が既にそのオブジェクトのロック設定をしているときに限り,そのオブジェクトに対して呼び出されなければならない。スレッド T が,対応するロック解除 動作によって対応が付いていない N個のロック設定(N lock) 動作を実行していたとする。メソッド wait は,現在のスレッドをそのオブジェクトの待機集合に追加し,現在のスレッドを,スレッドのスケジューリングの対象から無効にし,ロックを放棄するために N 回の ロック解除 を実行する。スレッド T は,次の三つのいずれかが起こるまで休眠状態になる。

これらが起こると,スレッド T が待機集合から削除され,スレッドのスケジューリング用に再度有効化される。その後,もう一度オブジェクトのロックにロックを設定する。(これは,通常の方法によって他のスレッドと競合することに関わってもよい。)一度ロックの制御を取得すると,さらにN-1 回の ロック設定 を実行し,メソッド wait の呼出しから戻る。したがって,メソッド wait から戻ったときは,オブジェクトのロック状態は,メソッド wait が呼び出されたときと同じになる。

メソッド notify は,現在のスレッドが,既にそのオブジェクトのロックにロック設定しているときに限り,そのオブジェクトに対して呼び出されなければならない。オブジェクトの待機集合が空でなければ,任意に選択されたスレッドが待機集合から削除され,スレッドのスケジューリング用に再度有効化される。(もちろん,このスレッドは,現在のスレッドが,そのオブジェクトのロックを放棄するまで先の処理に進むことはできない。)

メソッド notifyAll は,現在のスレッドが,既にそのオブジェクトのロックにロック設定しているときに限り,そのオブジェクトに対して呼び出されなければならない。待機集合内のそのオブジェクトに対するすべてのスレッドは待機集合から削除され,スレッドのスケジューリング用に再度有効化される。(もちろん,これらのスレッドは,現在のスレッドが,そのオブジェクトのロックを放棄するまで先の処理に進むことはできない。)


目次| |