目次 | 前 | 次 | 索引 | Java言語規定 第2版 |
これまでの記述の大半は,文又は式を,一度に一つ,つまり一つの スレッド(thread) で実行するコードの振舞いだけに関係していたが,Java仮想計算機は,一度に多数のスレッドを実行することができる。これらのスレッドは,共有主メモリに存在する値及びオブジェクトを操作するコードを独立して実行する。複数のスレッドは,複数のハードウェアプロセッサを搭載したり,一つのハードウェアプロセッサを時分割したり,又は複数のハードウェアプロセッサを時分割することによって,実現してもよい。
Javaプログラム言語は,スレッドの並行活動を 同期化する(synchronizing) 機構を提供することによって,並行的だが決定論的な振舞いを示すプログラムを支援する。スレッドを同期化するために,Javaプログラム言語では,モニタ(monitor) を使用する。モニタは,モニタが保護するコード領域を一度にただ一つのスレッドだけが実行可能にするための高度な機構とする。モニタの振舞いは,ロック(lock) によって表現する。各オブジェクトには,一つのロックが存在する。
synchronized
文(14.18) は,マルチスレッド操作にだけ関係する次の二つの特別な動作を実行する。(1) オブジェクトへの参照の計算後及びその本体の実行前に,そのオブジェクトのロックを ロック設定(lock) する。(2) オブジェクト本体の実行が正常完了又は中途完了した後,そのオブジェクトのロックを ロック解除(unlock) する。便宜上,メソッドを synchronized 宣言してよい。このようなメソッドは,その本体が synchronized 文に含まれているかのように動作する。
クラス Object
のメソッド wait
,notify
,及び notifyAll
は,一つのスレッドから他のスレッドへの効率的な制御の転送を提供する。スレッドは,計算資源を消費する,単なる"スピン"(内部状態の変化の有無を知るために,オブジェクトのロック設定とロック解除を繰り返すこと)ではなく,むしろ他のスレッドが notify
を使って目覚めさせるときまで wait
でそれ自体を一時停止させることができる。これは,スレッドが(共通の資源を共有するとき競合しないようにする)相互排他関係よりもむしろ,(共通の目標について積極的に協力する)生産者消費者関係をもつ状況に,特に適している。
スレッドは,コードを実行する際に一連の動作を実行する。スレッドは,変数の値を 使用(use) したり,又は変数に新しい値を 代入(assign) してよい。(その他の動作として,算術演算,条件検査,及びメソッド呼出しを含むが,これらは,直接には変数に関係しない。)複数の並行スレッドが,一つの共有変数に関して動作すれば,その変数への動作がタイミング依存な結果となることがある。このタイミング依存性は,並行プログラム固有のもので,Javaプログラム言語において,この規格だけではプログラムの実行結果が決定できない数少ない部分の一つとなる。
各スレッドは,作業メモリをもち,その中に,すべてのスレッドが共有する主メモリ上の変数値の複製を保持してもよい。共有変数にアクセスするためには,スレッドは,通常まずロックを取得し,その作業メモリをフラッシュする。これは,共有値が共有主メモリからスレッドの作業メモリにロードされることを保証する。スレッドがロックを解除するとき,作業メモリに保持された値が主メモリに書き込まれることを保証する。
ここでは,特定の低水準の動作に関する,スレッドの主メモリとの相互作用,及びスレッド同志の相互作用について規定する。これらの動作が発生してもよい順序についての規則が存在する。これらの規則は,Javaプログラム言語の任意の実装への制約を与え,プログラマは,規則に依存することで並行プログラムの可能な動作を予測できる。しかしながら,規則では,意図的に開発者へ自由裁量を与えている。その意図は,並行コードの実行速度及び効率の大幅向上を可能とする標準的なハードウェア及びソフトウェア技術を受け入れるためである。
long
値及び double
値には,特別な例外が存在する。17.4を参照すること。)
すべてのスレッドは,作業メモリ(working memory) をもつ。そこには,使用又は代入しなければならない変数の個別の 作業コピー(working copy) を保持する。スレッドがプログラムを実行する際は,この作業コピーに対して操作をする。主メモリは,すべての変数の マスタコピー(master copy) を含む。スレッドが変数の作業コピーの内容を,マスタコピーに転送する又はその逆を行うことをいつ許すか、又はいつ要求するかについての規則が存在する。
主メモリは,ロック も含む。それぞれのオブジェクトに関連した一つのロックが存在する。スレッドは,ロックを取得するために競合してもよい。
ここでは,動詞,使用,代入,ロード(load),記憶(store),ロック設定,及び ロック解除 を,スレッドが実行できる 動作(actions) を指すものとして使用する。動詞,読取り(read),書込み(write),ロック設定,及び ロック解除 は,主メモリサブシステムが実行できる動作を指すものとする。これらの各動作は,アトム的(atomic) (分割不能)とする。
使用 又は 代入 動作は,スレッドの実行エンジンとスレッドの作業メモリとの間の緊密な相互作用とする。ロック設定 又は ロック解除 動作は,スレッドの実行エンジンと主メモリとの間の緊密な相互作用とする。しかし,主メモリと作業メモリとの間のデータ転送は,緊密ではない。データを主メモリから作業メモリに複写するときには,必ず,次の二つの動作を伴う。すなわち,主メモリで実行される 読取り 動作と,それに引き続いて,ある時間後に,作業メモリで実行される対応する ロード 動作とが起こる。データを作業メモリから主メモリに複写する際も,必ず,次の二つの動作を伴う。作業メモリで実行される 記憶 動作と,それに引き続いて,ある時間後に,主メモリで実行される対応する 書込み 動作とが起こる。主メモリ及び作業メモリの間には,遷移時間(transit time)が存在してもよく,遷移時間は,トランザクションごとに異なってもよい。つまり,あるスレッドによって開始される異なる変数に対する動作は,他のスレッドからは,異なる順序に発生するように見えてもよい。しかしながら,個々の変数に対しては,任意の一つのスレッドについての主メモリの動作は,そのスレッドの対応する動作と同じ順序で実行される。(これは,次により詳細に説明する。)
一つのスレッドは,実行しているプログラムの意味規則によって指示された,使用,代入,ロック設定,及び ロック解除 の一連の動作を発行する。その下位レベルの処理系では,次に説明する制約に従うために,さらに,適切な ロード,記憶,読取り,及び 書込み 動作を実行することが要求される。処理系がこの規則に正しく従い,応用プログラマが特定の他のプログラミング規則に従っていれば,データは,共有変数を介してスレッド間で信頼できるように転送される。規則は,これを可能にするように十分"緊密"に設計されており,かつ,ハードウェア及びソフトウェアの設計者が,レジスタ,キュー,及びキャッシュなどの機構を介して実行速度及びスループットを自由に改善できるよう"自由度をもたせて"設計されている。
スレッド同志は,直接には相互作用しない。共有主メモリを介してだけ通信する。スレッドの動作及び主メモリの動作間の関係は,次の三つの制約を受ける。
次の規則において,"B は,A 及び C の間に介入しなければならない"という表現は,"動作 B は,動作 A に続き,動作 C に先行する"という意味とする。
+
のオペランドに出現すれば,V への一つの 使用 動作の発生を要求する。V が代入演算子 =
の左辺に出現すれば,一つの 代入 動作の発生を要求する。あるスレッドによるすべての 使用 及び 代入 動作は,スレッド実行プログラムが規定する順序で発生しなければならない。 T の次の動作として要求された 使用 の実行が,次の規則によって禁止されていれば,処理を進めるためには,T は,最初に ロード を実行する必要があるかもしれない。
主メモリ上で実行される 読取り 及び 書込み についても,次の制約が存在する。
volatile
宣言された変数に対しては,これよりも厳密な規則が存在する(17.7)。double
変数又は long
変数が volatile
宣言されていないとき,ロード,記憶,読取り,及び 書込み 動作の実行は,これらがそれぞれ32ビットの二つの変数であるかのように扱われる。これらの動作のいずれかが規則上要求されたときはいつでも,それぞれ32ビットの二回の動作として実行される。64ビットの double
変数又は long
変数を,二つの32ビット量として扱う方法は,実装依存とする。変数の型が double
または long
の場合であっても,volatile
宣言された変数へのロード,記憶,読取り,及び書込み動作はアトム的とする。
これは,double
又は long
変数の 読取り 又は 書込み が,実際の主メモリによって二つの32ビットの 読取り 又は 書込み として処理されることによって,時間的に分離され,その間に他の動作が入り込むことがあるということに関連している。その結果,二つのスレッドが,共有した同じ非 volatile
double
変数又は非 volatile
long
変数に,異なる値を並行して代入した場合,その変数を後で使用したとき,いずれの代入値とも等しくない,実装に依存した二つの値の混合値が得られることがある。
double
及び long
変数の ロード,記憶,読取り,及び 書込み 動作を,処理系は,アトム的な64ビット動作として実装してもよい,実際には,これを強く推奨する。本モデルは,64ビット量への効率的なアトム的メモリトランザクションを提供できない現在のマイクロプロセッサのために,32ビットずつに分割している。一つの変数について,すべてのメモリトランザクションをアトム的として定義した方が簡単である。この複雑な定義は,現在のハードウェア実装への現実的な譲歩である。将来には,この譲歩は,削除されるかもしれない。当分の間は,プログラマは,共有 double
変数,及び共有 long
変数へのアクセスは,常に明示的に同期化するように注意すること。
volatile
宣言されていれば,付加的な制約を各スレッドの動作に適用する。
T をスレッド,V 及び W を volatile
宣言された変数とする。
double
または long
の場合も,volatile
宣言された変数へのロード,記憶,読取り,及び書込み動作はアトム的とする。volatile
宣言されていなければ,これまでに説明した規則は,少し緩和され,記憶 動作を,これまでの規則が許すより前に行うことができる。この緩和の目的は,Javaコンパイラの最適化でコードの並べ替えを可能にするためとする。この時,適正に同期化されたプログラムの意味は,保存されるが,適正に同期化されていないプログラムでは,メモリ動作実行の順番が狂って実行されるかもしれない。T による V への 記憶 が,T による V への特定の 代入 に続くと仮定する。これまでの規則に従って,T による V への ロード 又は 代入 が介在しないものとする。この場合,記憶 動作は,代入 動作がスレッド T の作業メモリに入れた値を主メモリに送る。次の制約に従う限り, 記憶 動作が 代入 動作の前に発生してもよい。
あるスレッドが特定の共有変数を,特定のロック設定後にだけ使用し,その同じロックの対応するロック解除前でだけ使用しているならば,そのスレッドは,ロック設定 動作後に主メモリからその変数の共有値を読み取り,ロック解除 動作前に,必要であれば,その変数に代入された最新の値を主メモリに複写することになる。この規則は,ロックへの相互排他規則を併用することによって,共有変数を介して,一つのスレッドから他のスレッドに値が正しく転送されることを保証している。
volatile
宣言された変数に関する規則は, volatile
宣言された変数の主メモリは,それぞれの 使用 及び 代入 ごとに,厳密に一度だけスレッドによってアクセスされること,及びその主メモリは,そのスレッドの実行意味規則によって指示された順序でアクセスされることを要求している。しかし,volatile
宣言されていない変数への 読取り 及び 書込み 動作に関しては,そのようなメモリ動作は,要求されていない。
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
の 書込み が a
の 読取り に先行すること,及び,b
の 書込み が b
の 読取り に先行すること,の両方が不可能なこととなる。その理由は,上図の因果律を表す矢印がループ状を形成し,その結果,ある動作がそれ自体に先行することになるからであり,これは許されない。必須ではない 記憶 及び 書込み 動作が発生すると仮定すると,主メモリが規則に従って動作を実行する順序は,3通り存在する。ha
及び hb
を hither
スレッド用の a
及び b
の作業コピーとし,ya
及び yb
を yon
スレッド用の作業コピーとし,ma
及び mb
を主メモリ内のマスタコピーとする。初期値は,
ma=1
及び mb=2
とする。この場合,3通りの動作の可能な順序及びその結果状態は,次の通りとする。
a
read a
, read bwrite b (このとき ha=2
, hb=2
, ma=2
, mb=2
, ya=2
, yb=2
)
a
write a
, write b
read b
(このとき ha=1
, hb=1
, ma=1
, mb=1
, ya=1
, yb=1
)
a
write a
, read b
write b
(このとき ha=2
, hb=2
, ma=2
, mb=1
, ya=1
, yb=1
)
b
が a
に複写される,a
が b
に複写される,又は a
及び b
の値が交換されるのいずれかとなる。さらに,変数の作業コピーは,一致する場合も一致しない場合もある。これらの結果のどれか一つが他の結果よりも適切と仮定することは正しくない。これは,プログラムの振舞いが必然的にタイミング依存になる一例となる。実装によっては,記憶 及び 書込み 動作の両方を実行しないようにしたり,そのいずれかだけを実行しないようにするかもしれない。この場合,実装によって可能な,また別の結果を生じる。
次に,この例を、synchronized
メソッドを使用して修正したとする。
もう一度,class SynchSample { int a = 1, b = 2; synchronized void hither() { a = b; } synchronized void yon() { b = a; } }
hither
を呼び出すスレッドを考える。規則に従うと,このスレッドは,メソッド hither
の本体を実行する前に(メソッド hither
を呼び出しているクラス SynchSample
のインスタンスについての),ロック設定 動作を実行しなければならない。その後で b
の 使用 及び a
の 代入 動作が続く。最後に,メソッド hither
の本体が終了した後で,SynchSample
の同じインスタンスへの ロック解除 動作を実行しなければならない。これを,メソッド hither
の呼出しを実行するために要求される最低条件とする。
前述のように,b
の ロード が要求され,結果的に,この ロード が先行する主メモリによる b
の 読取り 動作を要求する。ロック設定 動作の後に ロード が行われるので,対応する 読取り も ロック設定 動作の後でなければならない。
ロック解除 動作が a
の 代入 に続くので,a
への 記憶 動作は,必須とする。この 記憶 は,結果的に,後続する主メモリによる a
の 書込み 動作を要求する。その 書込み は,ロック解除 動作に先行しなければならない。
yon
を呼び出すスレッドに関する状況も類似であるが,a
及び b
の役割は逆転している。
a
read a
, read b
write b
(このとき ha=2
, hb=2
, ma=2
, mb=2
, ya=2
, yb=2
)
a
write a
, write b
read b
(このとき ha=1
, hb=1
, ma=1
, mb=1
, ya=1
, yb=1
)
a
及び b
の値が一致することがわかる。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
を取得してよい。
次に,to
を synchronized
とし,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
の 書込み の前に発生することを要求していないし,a
の 読取り は, b
の 読取り の前に発生することも要求していないことに注意すること。さらに,メソッド to
が synchronized
であっても,メソッド 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
" を出力する。Thread
及び ThreadGroup
によって,生成及び管理される。Thread
オブジェクトを生成するとスレッドが生成されるが,これをスレッドを生成する唯一の方法とする。スレッドは,生成時点では,まだ活動的にはなっていない。スレッドは,メソッド start
が呼び出されると,実行を開始する。すべてのスレッドは,優先度(priority) をもつ。処理資源に関して競合が存在するとき,一般には,優先度の高いスレッドが優先度の低いスレッドに優先して実行される。ただし,このような優先度は,最も高い優先度のスレッドが常に実行されることを保証するものではない。高い信頼性で相互排他を実装するためには,スレッドの優先度を使用することはできない。
ただし,Java仮想計算機では,ロック設定 及び ロック解除 動作を実装する,独立したmonitorenter 命令及び monitorexit 命令を提供していることに注意せよ。
synchronized
文 (14.18)は,オブジェクトへの参照を計算する。その後で,そのオブジェクトへの ロック設定 動作の実行を試み,ロック設定 動作が正常完了するまで先の処理に進まない。(ロック設定 動作は,遅延してもよい。その理由は,ロックに関する規則は,ある他のスレッドが一つ以上の ロック解除 動作を行う準備ができるまで,主メモリの参加を禁止することができるからである。)ロック設定 動作が実行された後で,synchronized
文の本体が実行される。本体の実行が,正常完了又は中途完了のいずれかで終了した場合,その同じロックへの ロック解除 動作が自動的に実行される。
synchronized
メソッド(8.4.3.6)は,呼び出されたときに,自動的に ロック設定 動作を実行する。その本体は,ロック設定 動作が正常に完了するまで実行されない。メソッドがインスタンスメソッドならば,それに対して呼び出されたインスタンス(つまり,そのメソッドの本体の実行中に this
として参照されるオブジェクト)に関連するロックにロック設定する。メソッドが static
ならば,メソッドが定義されたクラスを表すClass
オブジェクトのロックにロック設定する。本体の実行が,正常完了又は中途完了のいずれかで終了した場合,その同じロックへの ロック解除 動作が自動的に実行される。
ある変数が,ある一つのスレッドで代入され,他のスレッドで使用又は代入される場合,その変数に対するすべてのアクセスは,synchronized
メソッド又は synchronized
文内に囲まれなければならない,というのが最良の方法となる。
Javaプログラム言語は,デッドロック状態を,防止しないし,検出を要求することもしない。スレッドが,複数のオブジェクトへのロックを(直接的又は間接的に)保持するプログラムは,必要ならば,デッドロックが生じない高水準のロックプリミティブを作成して,従来からあるデッドロック回避手法を使用しなければならない。
待機集合は,クラス Object
のメソッド wait
,メソッド notify
,及びメソッド notifyAll
が使用する。これらのメソッドは,スレッドのスケジュール機構とも相互作用する。
メソッド wait
は,現在のスレッド(T と呼ぶ)が既にそのオブジェクトのロック設定をしているときに限り,そのオブジェクトに対して呼び出されなければならない。スレッド T が,対応するロック解除 動作によって対応が付いていない N個のロック設定(N lock) 動作を実行していたとする。メソッド wait
は,現在のスレッドをそのオブジェクトの待機集合に追加し,現在のスレッドを,スレッドのスケジュールの対象から無効にし,ロックを放棄するために N 回の ロック解除 を実行する。スレッド T は,次の三つのいずれかが起こるまで休眠状態になる。
notify
を呼び出し,スレッド T が偶然に通知先として選択される。
notifyAll
を呼び出す。
wait
の呼出しがタイムアウト時間を指定していて,指定された実時間が経過した。
wait
の呼出しから戻る。したがって,メソッド wait
から戻ったときは,オブジェクトのロック状態は,メソッド wait
が呼び出されたときと同じになる。
メソッド notify
は,現在のスレッドが,既にそのオブジェクトのロックにロック設定しているときに限り,そのオブジェクトに対して呼び出されなければならない。オブジェクトの待機集合が空でなければ,任意に選択されたスレッドが待機集合から削除され,スレッドのスケジュール用に再度有効化される。(もちろん,このスレッドは,現在のスレッドが,そのオブジェクトのロックを放棄するまで先の処理に進むことはできない。)
メソッド notifyAll
は,現在のスレッドが,既にそのオブジェクトのロックにロック設定しているときに限り,そのオブジェクトに対して呼び出されなければならない。待機集合内のそのオブジェクトに対するすべてのスレッドは待機集合から削除され,スレッドのスケジュール用に再度有効化される。(もちろん,これらのスレッドは,現在のスレッドが,そのオブジェクトのロックを放棄するまで先の処理に進むことはできない。)
目次 | 前 | 次 | 索引 | Java言語規定 第2版 |