3. スレッドの排他制御
- 3.1. synchronizedブロック
- 3.2. synchronizedブロックの仕組み
- 3.3. synchronized メソッド
- 3.4. static synchronized メソッド
- 3.5. volatile 変数
複数のスレッドが同じオブジェクトを同時に操作すると、プログラムが予想外の動作をすることがあります。そこでこの章では、複数のスレッドの動作を制御し、同じオブジェクトが同時に操作されないようにする方法を説明します。
3.1. synchronizedブロック
2つのスレッドは、同じオブジェクトを並行して同時に扱うことができます。2つのスレッドがあるオブジェクトのフィールド変数を同時に書き込んだりすると、プログラムが時として意図しない動作をすることがあります。
例として、預金口座を表すクラスを考えてみましょう。このクラスには、お金を振り込むためのdepositメソッドが定義されています。
class Account { private int balance = 0; // 預金残高 public void deposit(int money){ int total = balance + money; balance = total; } }
このプログラムにはなにも問題がないように見えます。たしかに、同じインスタンスが1つのスレッドからしか利用されないのであれば問題はありません。しかし、同じインスタンスが複数のスレッドから同時にアクセスされたとき、問題が生じるおそれがあります。
balanceが0である状態のときに2つのスレッドから同じインスタンスのdeposit(1000)を呼んだとしましょう。2つのスレッドが次のようなタイミングで並列動作したらどうなるでしょうか。
スレッド1
|
スレッド2
|
int total = balance + money; | |
int total = balance + money; | |
balance = total; | |
balance = total; |
totalはメソッドで定義されたローカル変数ですので、メソッドの呼び出しごとに個別に領域確保されます。つまり、それぞれのスレッドで別々の領域を使用してします。両方のスレッドでtotalへの代入が終わったとき、どちらのスレッドのtotalの値も1000になっています。balanceはインスタンス変数ですので、スレッド1とスレッド2で領域を共有しています。スレッド1で1000(スレッド1のtotalの値)が代入され、スレッド2でも再度1000(スレッド2のtotalの値)が代入されます。つまり、最終的にbalanceの値は1000となります。
結果を見てみると、deposit(1000)を2回実行したにもかかわらずbalanceが1000しか増えていません。銀行で、2箇所から1000円振り込んでもらったのに、1000円しか預金が増えてなかったら困りますね!
それでは次のように1文で処理を書いてしまえばよいのでしょうか。
public void diposit(int money){ balance += money; }
これなら絶対にさっきのような間違いは発生しないように思えます。しかし、これでも誤動作の可能性があります。それぞれのスレッドは内部的に固有の作業コピー領域を持っており、より高速にプログラムを実行するために、変数の値を一時的にスレッド固有のメモリにコピーして作業することが許されています。この例では、balanceの値を共有メモリからスレッド固有メモリにコピーし、そこで加算処理を実施してから共有メモリに書き戻す、という処理を行います。つまり、このように1行でプログラムを書いたとしても、先ほどのプログラムと同じようなことが内部的に実行されてしまうのです。
この問題を解決するにはsynchronizedブロック(またはsynchronizedメソッド)を利用します。
public void deposit(int money){ synchronized(this){ int total = balance + money; balance = total; } }
synchronizedブロックがどのような意味を持つのかは、次の節で説明します。