[???] /
[Java FAQ] / [S021]
S021: スレッド - thread(同期と排他制御 - synchronization / mutual exclusion)
[S021 Q-01]
new Thread(ThreadGroup, Runnable) を実行すると、
IllegalThreadStateException が発生します。なぜ?
[S021 A-01]
S019-19 を参照。
[S021 Q-02]
メソッドに付ける synchronized って何ですか?
[S021 A-02]
インスタンスをスレッド間で排他的に利用するための宣言です。
他のスレッドが既にそのインスタンスを利用している時に、
別のスレッドがこのメソッド実行しようとすると、前者の処理が
終わるまで、後者は実行を待たされるようになります。
排他の範囲はメソッドではなくインスタンスであることに
注意して下さい。すなわち、 synchronized は
「メソッドを排他的に実行するための宣言」ではありません。
インスタンスが異なれば待たされずに同時に実行されますし、
synchronized を付けた別のメソッドであっても
同じインスタンスに対するものならば同時に実行されません。
static なメソッドに付けた場合は、そのクラスの Class
インスタンスが排他の範囲になります。簡単に言えば、
クラスを排他的に利用する宣言になります。
synchronized の使い方の詳細は、[S021-05]を参照して下さい。
[S021 Q-03]
synchronized は、どういうとき必要なのでしょうか?
[S021 A-03]
あるインスタンスを
複数のスレッドが同時に操作すると、
お互いの動作の邪魔をしてしまう可能性がある場合に必要です。
例えば、オブジェクトに複数の関連の有る値が有った時に、
これらをあるスレッドが書き変えている最中に、
別のスレッドが読み出してしまうと、
変更途中の誤った(矛盾した)値を読んでしまう事になります。
この場合には synchronized を使って、
読み書きが同時に起こらないようにします。
アルゴリズムを変えてお互いの邪魔をしないようにできれば、
synchronized を使う必要は有りません。
synchronized を使うと実行時間がよりかかるので、
必要が無ければ使わないほうが良いでしょう。
[S021 Q-04]
synchronized を使わないようにするアルゴリズムのヒントは?
[S021 A-04]
インスタンス変数の値をローカル変数に取り出してから、
そちらを扱うようにする方法が使える場合が有ります。
インスタンス変数(あるいはクラス変数)を使い一連の操作をしている時に
別のスレッドが変数の内容を変化させると、変化の前後で操作内容が
矛盾しまうことが有ります。synchronized を使うことでこれを避けら
れますが、変数が一つの場合には内容をローカル変数に取り出して
おくことによっても避けられます。
インスタンス変数やクラス変数はスレッド間で同じ物が使われますが、
ローカル変数はスレッド毎に異なるものが用意されるので、その部分は
干渉の心配が無くなります。
ただしこの方法は、別のスレッドがその変数そのものの値を
変える場合にのみ使えます。その変数が参照するオブジェクトの
内容が変化する場合には使えません。
別の方法として、スレッド間でインスタンスを共有せずに、
それぞれ異なるオブジェクトを使うようにする、というのが考えられます。
別々のインスタンスを扱うようになれば、相互干渉の心配は無くなります。
この場合には java.lang.ThreadLocal を使うと便利でしょう。
[S021 Q-05]
synchronized はどのように使うのでしょうか?
[S021 A-05]
お互いに同時に実行すると具合の悪い部分を synchronized 構文で囲みます。
メソッドに synchronized を付ける方法も有りますが、
synchronized 構文がより基本的なのでまずはこちらから説明します。
synchronized 構文は、次のように書きます。
文が書けるところならばどこにでも書けます。
synchronized(排他的に利用したいインスタンスの式) {
同時に実行すると具合の悪い部分
}
同じインスタンスに対してこれが書いてある部分は、
排他的に実行されます。
すなわち、あるスレッドが大括弧で囲われた部分を実行している時には、
別のスレッドは括弧内部の実行を開始するのを待たされます。
実行していたスレッドが括弧内部から抜け、
実行しているスレッドがいなくなると、
待たされていたスレッドが括弧内を実行できるようになります。
(待たされていたスレッドが複数ある時には、
そのうちのどれかが一つだけが実行できるようになります。)
排他的な実行が、
丸括弧内に書かれるインスタンス毎に管理されていることに注意して下さい。
別のところに書かれた synchronized であっても、
インスタンスが同じであれば排他制御されますし、
インスタンスが異なれば、
同じ所であっても同時に実行されます。
括弧内部を実行しているスレッドが、
一時的に他のスレッドに実行を譲るには Object#wait() を使います。
さて冒頭に述べたメソッドに synchronized を付ける方法の場合、例えば
synchronized public void anInstanceMethod() {
...
}
は、
public void anInstanceMethod() {
synchronized (this) {
...
}
}
の意味です。
クラスメソッドの場合には、
排他利用のインスタンスが this ではなく、
そのクラスのクラスオブジェクトになります。
すなわち、
static synchronized public void anClassMethod() {
...
}
は、
static public void anClassMethod() {
synchronized (このクラス名.class) {
...
}
}
と同じ意味です。
複数のインスタンス間で互いに排他が必要になる場合には、
排他を行うためのオブジェクトを共有する手法があります。
次は、二つのクラスのインスタンスがお互いに排他を必要とする時の例です。
class AClass {
final Object LOCK = new Object();
void method1() {
synchronized (LOCK) { ... }
}
}
class BClass {
AClass a;
void method1() {
synchronized (a.LOCK) { ... }
}
}
[S021 Q-06]
Object#wait() や Object#notify() を実行すると
java.lang.IllegalMonitorStateException
がおきるのですが?
[S021 A-06]
wait() や notify() は、synchronized () {...} のブロックの内部
(あるいは synchronized がつけられたメソッドの内部)
に入っていないと使えません。
wait() は、
既に排他的に実行することが出来るようになったスレッドが、
実行を待っているスレッドに実行を一時的に譲るのに使います。
呼んだスレッドは、“寝ている”状態になります。
notify() は、
排他的に実行しているスレッドが、
自分が実行を終えた(あるいは wait で停止した)時に
先に wait を実行して“寝ている”スレッドが再び動き出すように、
それらを“実行を待つ状態”にするのに使います。
[S021 Q-07]
synchronized を使ったらプログラムが止まってしまいました。なぜ?
[S021 A-07]
synchronized を出るための条件を検討し直してみて下さい。
条件が満たせずに抜けられないスレッドがあるはずです。
また、出ないスレッドがあるために、
入れないで止まっている場合も有ります。
以下にいくつかのケースを示します。
1.
Object#wait() を使用している場合には、
wait()で止まったまま synchronized を出られない可能性が有ります。
この場合は Object#notify() をどのスレッドが呼ぶのかをチェックし直します。
例えば、複数のスレッドが、お互いが notify を呼んでくれると
間違って判断してしまい Object#wait() を呼んで止まってしまうと、
いつまでも wait の状態のままになってしまいます。
2.
synchronized を入れ子に使う場合には、特別な注意が必要です。
複数のスレッドが互いに、
「自分が synchronized を出るには相手が synchronized を出る必要が有る」
という状況になり、止まってしまうことが有り得ます。
次のような場合にこれが起こります。
入れ子になっている
synchronized (a) {
synchronized (b) {
...
}
}
というコードがあるとします。
二つのスレッドが有り、
参照している a と b のインスタンスの組あわせが
互いに逆になっている場合に同時にこのコード実行したとします。
すると、両方のスレッドが止まることがあります。
なぜなら、二個目の synchronized に入るところで、
お互いの一個目の synchronized を出るのを待ってしまうからです。
解決には次の方法が有ります。
a と b が異なる種類のものであれば、
つまり継承や実装の関係が無いのであれば、
常に同じ順序で synchronized を入れ子にします。
同じ種類のものであり、
インスタンスが逆の関係になって実行される可能性がある場合には、
必ずインスタンスが一定の順で synchronized 構文に入るようにします。
これには、System.identityHashCode を利用すると良いでしょう。
hash code の大小によってインスタンスを入れ替えてから、
synchronized に入るようにします。
例:BitSet#and()
[S021 Q-08]
Object#notifyAll() を使ってますが、
動き出すスレッドの順番がばらばらです。なぜ?
[S021 A-08]
動き出す順番は決まっていません。
wait() を実行した順番にはなりません。
順番に実行したいのならば、
別途待ち行列を作って先頭のスレッドだけが
動くような仕組みを作る必要が有るでしょう。
[S021 Q-09]
二つのスレッドの処理が、それぞれあるところまで終わるまで、
先に終わった方のスレッドの実行を止めたいのですが?
[S021 A-09]
先に終わった方が Object#wait() を使い、
後から終わったほうが Object#notify() を使い、
スレッドを起こしてやります。
例えば、次のようにします。
Object lock = new Object();
boolean arrived = false;
void rendezvous() {
synchronized (lock) {
if (arrived) {
lock.notify();
} else {
arrived = true;
try {
lock.wait();
} catch (InterruptedException e) {
throw new Error();
}
}
}
}
一方のスレッドが終わるのを待つのであれば、
Thread#join を使う方が簡単です。
[S021 Q-10]
コンストラクタには synchronized の指定が出来ませんが、
大丈夫ですか?
[S021 A-10]
コンストラクタを synchronized にする必要の有る状況は
通常起こらないので問題ありません。
コンストラクタを synchronized にすることは実質的な意味が無いために、
言語仕様で synchronized にできないことになっています。
なぜなら仮に出来たとすると、これは、
作成途中のオブジェクトを他のスレッドから利用できないという事を意味します。
ところが、このオブジェクトは作成途中であり、
まだ利用できる状態に成っていないので、
通常はそもそも利用されることは有りません。
よって、コンストラクタを synchronized にすることには意味が無いと言えます。
もし、コンストラクト中にそのオブジェクトを
他のスレッドから利用される状況にしている場合には注意が必要です。
(他のスレッドが処理する集合に加えるなど)
この場合には、コンストラクト中の不完全な状況でも利用されて
問題が無いようにしなければなりません。
これが考慮されていないのであれば、
コンストラクタ中でこのようなことはしてはいけません。
(synchronized を使って問題が無いようにする方法は、
S021-11 を参照して下さい。)
なお、コンストラクタの最後であれば作成は完了しているので
集合にに加えるなどしても問題ないと思うかもしれませんが、
そのクラスを継承する必要が生じた時に困る可能性が有るので
やはりお勧めは出来ません。
その時点で、サブクラスの初期化は完了していないからです。
[S021 Q-11]
synchronized を使って、コンストラクタの実行が終る前に
別スレッドがそのオブジェクトのメソッドを呼べないように
できないでしょうか?
[S021 A-11]
synchronized(this){...} により条件付きながら可能です。
(この方法を使う前に「コンストラクタを実行中にメソッドが呼ばれるのは
特殊であり、気を付けなければいけない問題がある」ということを理解して
おいて下さい。S021-10 で紹介しています。)
コンストラクタには synchronized の指定が出来ませんが、
コンストラクタ中で synchronized(this){...} を使うことは出来ます。
コンストラクタの内容を synchronized(this) で囲み、
各メソッドに synchronized を付ければ目的が達せられます。
しかしこの方法はサブクラス化した時に、問題を起こす可能性が有ります。
サブクラスのコンストラクタ(すなわち初期化)が終了しないうちに、
メソッドが呼ばれる可能性が有るためです。
サブクラスのコンストラクタにも synchronized(this) {...}
を書けばいいように思うかもしれませんが、それではだめです。
スーパクラスのコンストラクタは先頭に書かないと
いけないため、コンストラクタ 呼び出しが synchronized の外側になり、
スーパクラスのコンストラクタが終わった後、
一瞬 synchronized を出る事になってしまうからです。
[S021 Q-12]
寝ているスレッドを起こすには?
Thread.sleep(long) と Thread#interrupt() を使って
スレッドを止めたり動かしたりしているのですが、
うまくいかない場合が有ります。
[S021 A-12]
S019-20 を参照。
contributor: Norikazu Nakato
コメントの送り先 Java FAQ BBS