※この記事にはjava言語を使用しています。
ロック
ロックとは「鍵」のことです。部屋に1人でいたとき、部屋に入って鍵をかけます。
そうすれば、鍵を開けて部屋を出るときまで、他の人が部屋にはいってくることはありません。
スレッドは、synchronizedが付いたメソッド(synchronizedメソッド、同期メソッド)に入るとき鍵をかけ、そのメソッドからでるときに鍵をはずします。
これにより、そのメソッドに入れるスレッドは1つだけになります。つまり、
① synchronizedメソッドに入って鍵をかける
② 1人で仕事をする。
③ 鍵をはずしてsynchronizedメソッドから出る。
ということです。
ところでスレッドが鍵をかける部屋とは何でしょうか。スレッドがsynchronizedメソッドに入るときには、そのインスタンスにに対して鍵をかけているにです。
あるスレッドがsynchronizedメソッドに入ると、そのインスタンスに対してロックをかけます。すると、他のスレッドは、同じインスタンスのsynchronizedメソッドに入ることはできません。1人の小人(スレッド)がその部屋(インスタンス)に鍵をかける(ロックをかける)と、他の小人はその部屋(インスタンス)の鍵のある部屋(synchronizedメソッド)には入れないのです。
一般に次のことが言えます。
① あるスレッドがsynchronizedメソッドを実行している最中であっても、そのインスタンスのsynchronizedではないメソッドならば、どのスレッドも実行できる。
② あるスレッドがsynchronizedメソッドを実行していても、別のインスタンスのメソッドは、(synchronizedであってもなくても)実行できる。
③ 1つのスレッドは、1つのインスタンスのsynchronizedメソッドを続けて実行できる(自分自身が鍵によって締め出されることはない)
スレッドはメソッドに対して鍵をかけているのではなく、インスタンスに対して鍵をかけているのだ、という事をよく覚えておいてください。
インスタンスに鍵をかけようとしたとき、すでにそのインスタンスに鍵がかかっていたら、鍵をかけようとしたスレッドはまたされることになります。
そのように待たされるスレッドは1つではないかも知れません。ある小人がsynchronizedメソッドを実行している間、たくさんの小人たちがずらりと待たされてしまうかもしれません。
同期処理
同時に複数のスレッドを動かす場合、考えなければいけないのは「あるオブジェクトに同時に複数のスレッドからアクセスがあるかもしれない」ということです。たとえば、あるオブジェクトの値を変更するなど処理をしているとき、同時に複数のスレッドから値の変更がなされたらどういうことになるでしょう?
あるいは、オブジェクトの値を変更してからその値をまた取り出す、というようなとき、変更してから取り出すまでの間に他のスレッドが値を操作してしまったら?
この様な場合を考えると「この処理をしている間は、このオブジェクトは他のスレッドから一切アクセスできない」というような操作が必要となります。そのための制御機構が、このsynchronizedなのです。
メソッドにこのsynchronizedをつけると、そのメソッドの処理中は、メソッドで使用しているオブジェクトに他のスレッドからアクセスすることができなくなります。
synchronizedブロック
メソッド単位でなく、もっと細かに処理の同期を行いたい場合、こんな具合に書くこともできます。
synchronized(オブジェクト){
・・・処理
}
こうすると、この部分の処理を実行している間は、引数に指定したオブジェクトに他のスレッドからアクセスでくなくなります。
「アクセスできなくなる」というのは、つまり「アクセスが許可されるまで、そのスレッドには待たされる」ということです。synchronizedの部分を抜けたら、それまで待たされていたスレッドがオブジェクトにアクセスできるようになる、というわけです。
サンプル
class Sample{
Integer count = new Integer(1);
//カウントアップするメソッド
void countUp(){
//1つのスレッドのみが実行
//このブロック(synchronized)内の処理をしている間は他のスレッドからは
//countオブジェクトにはアクセスできない(ロック)
//ブロック内の処理が終了したらロックを開放
synchronized(count){
int temp = count.intValue();
temp = temp + 1;
try{
//スレッド切り替え(ロック開放)までの時間
Thread.sleep(3000);
}catch(InterruptedException e){
e.printStackTrace();
}
count = new Integer(temp);
}
}
//スレッド生成のネストクラス
class CountThread extends Thread{
String tname;
CountThread(String tname){
this.tname = tname;
}
@Override
public void run(){
countUp();
System.out.println(tname+" count="+count.intValue());
}
}
void doSample(){
//スレッド1作成
Thread t1 = new CountThread("スレッド1");
t1.start();
//スレッド2作成
Thread t2 = new CountThread("スレッド2");
t2.start();
}
public static void main(String[] args){
System.out.println("メインスレッド");
new Sample().doSample();
}
}
出力結果
メインスレッド
スレッド1 count=2 //メインスレッドが表示されて3秒後に表示
スレッド2 count=3 //スレッド1が表示されて3秒後に表示
上記の例では記述を簡単にするために、ネストクラスを使っています。
まず外観のSampleクラスでは、countというinteger型変数と、countUpというメソッドを定義しています。count変数は初期値1です。countUpメソッドの中では、「countの値に1を加算する」という処理を行っています。
行いたい処理は一言で言うと「count++」なのですが、同時にアクセスで問題が起こりやすくするために、わざと一時的変数に値をコピーしたり、sleepメソッドでスリープしたりしています。ネストクラスであるCountThreadでは、countUpメソッドを呼び出し(ネストクラスなので外側のクラスのインスタンスメソッドにアクセスできます)、countの値を画面に表示しています。
mainメソッドでは、Sampleのインスタンスを作成してdoSampleメソッドを呼び出しています。doSampleメソッドでは、CounterThreadクラスのインスタンスを2つ作って、それぞれstartメソッドを呼んで、スレッドを開始しています。
もしcountUpメソッド内をsynchronizedブロックで囲んでいない場合は下記の出力結果になります。
出力結果
メインスレッド
スレッド1 count=2
スレッド2 count=2
synchronizeで囲んだブロックには同時に1つのスレッドしか入れないことになっており、そのためにロックという仕組みが利用されます。
ロックはインスタンスに対してかけることになっており、synchronized文でロック対象のインスタンス変数を指定します。上記の例ではcountを指定しています。このロックの対象にできるのはオブジェクト(インスタンス)で、intなどの基本データ型は指定できません。