複数のスレッドが動いているときに、1つのスレッドがある変数Aをいじっている最中に、スレッドが切り替わって別のスレッドからAを書き換えるようなことが起きると不便なことがある。勝手にスレッドは動作するため、スレッドを制御する方法としてsynchronizedという修飾子を用いた、排他制御の仕組みが用意されている。排他制御はその名の通り、スレッドが実行している間、synchronized修飾子がかかっている箇所にロックをかけて、他のスレッドはロックがかかっているうちはアクセスができないようにすることである。
以下の例では、複数のスレッドが、計算を繰り返し、その結果をBadのvalueに記録するというものである。スレッド1が1を足して、スレッド1が抜ける前にスレッド2が1を足すということになると、エラーで停止してしまう。排他制御を行わないと、スレッド1がadderを実行している最中に、スレッド2がadderを実行してしまうのである。
BadTest.javaの中身
public class BadTest extends Thread{
Bad test;
public BadTest(Bad bd){
this.test = bd;
}
public void run(){
for( int i=0 ; i<10 ; i++ ){
test.adder(1);
}
}
public static void main(String[] args){
Bad bd = new Bad();
new BadTest(bd).start();
new BadTest(bd).start();
}
}
Bad.javaの中身
public class Bad{
private int value = 0;
public void adder(int v){
int tmpValue = value;
System.out.println(Thread.currentThread() + " is at adder");
value += v;
if( tmpValue + v != value ){ // 単純に足しただけだが...
System.out.println(Thread.currentThread() + " conflicted!");
System.exit(-1); // 異常終了
}
System.out.println(Thread.currentThread() + " get out of adder");
}
}
上のコードをコンパイルして実行すると次のようになり、矛盾が発生していることが分かる。
d:\java>javac BadTest.java Bad.java // ファイルをまたいでいるので、まとめてコンパイル
javac BadTest.java Bad.java
d:\java>java BadTest
java BadTest
Thread[Thread-0,5,main] is at adder
Thread[Thread-1,5,main] is at adder
Thread[Thread-0,5,main] get out of adder
Thread[Thread-1,5,main] conflicted! // ここで矛盾
Thread[Thread-0,5,main] is at adder
Thread[Thread-0,5,main] get out of adder
この矛盾を解決するのがsynchronized修飾子である。synchronizedは以下のようにadder関数に修飾するとよい。
public synchronized void adder(int v)
これによって、一つのスレッドがadder関数を実行している間、同じインスタンスを利用する別スレッドからはadder関数が利用できなくなり、矛盾(=conflicted!の表示)は発生しなくなる。また、synchronized修飾子はメソッドにもブロックにもかけられる。ブロックにsynchronized修飾子をつける場合は、ロックをかけるオブジェクトを指定する必要がある。
public void adder(int v){
synchronized(this){
...
}
}