【Java】スレッドの同期

2018年3月11日

本稿はマルチスレッド間の同期処理を実装する方法について説明します。

マルチスレッドは一つのデータに複数のスレッドでデータの書き換えを行うような処理をしてしまうと、意図しない結果となってしまう可能性があります。

スレッド間同期をする事によってある別スレッドの処理が、終了するまでスレッドを待機するような処理を実装することができます。

この同期をうまく利用することで、データの整合性を保つことができるのですね。

synchronizedブロック

同期を利用する方法としてsynchronizedブロックを利用する方法があります。

まずは、同期処理が必要そうなプログラムを適当に例に出します。

ふたりのキャラのうちどちらかが5回連続攻撃アクションさせ、次に攻撃していないキャラが5回連続で攻撃アクションさせたいプログラムを実装したいとします。

以下のような実装になりました。

public class Test{
	public static void main(String[] args){
		Chara boy = new Chara("大剣使いの少年");
		Chara girl = new Chara("魔法使いの少女");
		boy.start();
		girl.start();
	}
}
class Chara extends Thread{
	String name;//キャラクタの名前。
	Chara(String name){
		this.name = name;
	}
	public void run(){
		Battle.process(this);
	}
}
class Battle{
	static void process(Chara ch){
		for ( int i = 0;i < 5;i++ ) {
			System.out.println(ch.name+"は攻撃しました。");
		}
	}
}
実行結果

大剣使いの少年は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
魔法使いの少女は攻撃しました。

もしかしたら、うまくいく可能性もありますが途中で違うキャラが攻撃処理してしまうほうが多いと思います。

こういう時にはsynchronizedブロックを使用して、同期処理を行うとうまくいきます。

synchronizedブロックは

synchronized(ロックオブジェクト){
}

と指定しましょう。

それでは、synchronizedを使用して試してみましょう。

public class Test{
	public static void main(String[] args){
		Chara boy = new Chara("大剣使いの少年");
		Chara girl = new Chara("魔法使いの少女");
		boy.start();
		girl.start();
	}
}
class Chara extends Thread{
	String name;//キャラクタの名前。
	Chara(String name){
		this.name = name;
	}
	public void run(){
		Battle.process(this);
	}
}
class Battle{
	static Object lock = new Object();
	static void process(Chara ch){
		synchronized(lock){
			for ( int i = 0;i < 5;i++ ) {
				System.out.println(ch.name+"は攻撃しました。");
			}
		}
	}
}
実行結果

魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。

ここで出てきたロックオブジェクトですが、次のようなことが起こります。

スレッドがsynchronized(ロックオブジェクト)の部分を通ると、指定したオブジェクトにスレッドがロックをかけなければなりません。

そのため、スレッドがsynchronized(ロックオブジェクト)を通った時にそのオブジェクトにロックをかけます。

ロックの解除はロックをかけたスレッドしかできません。

オブジェクトにロックがかかってしまうと、他のスレッドがオブジェクトにロックをかけられなくなるので、ロックが解除されるまで待機するようになります。

このようなイメージで内部処理が行われます。あくまでもイメージ。

よって、他の場所にも同じオブジェクトを指定すれば、別メソッド間だろうが、別クラス間だろうが、スレッド待機させることができます。

・・・といいましたが別クラスのレベルまではやらないほうがいいです。

理由はわかりにくくなるからですね。

synchronizedメソッド

synchronizedをメソッドに付与することができます。

この指定は、インスタンスメソッドならばsynchronized(this)でメソッド内の処理をすべて囲った状態と同等の動きになります。

staticメソッドに付与した場合static変数としてロックオブジェクトを作成し、それをsynchronizedブロックに指定し、メソッド内の処理をすべて囲った状態と同等の動作になります。簡単に言うと、同クラスのstaticメソッド間で同期が可能になります。

synchronizedメソッドは

synchronized 戻り値 メソッド名(){
}

のように記述します。

それでは、synchronizedメソッドのサンプルも見ておきましょう。

public class Test{
	public static void main(String[] args){
		Chara boy = new Chara("大剣使いの少年");
		Chara girl = new Chara("魔法使いの少女");
		boy.start();
		girl.start();
	}
}
class Chara extends Thread{
	String name;//キャラクタの名前。
	Chara(String name){
		this.name = name;
	}
	public void run(){
		Battle.process(this);
	}
}
class Battle{
	synchronized static void process(Chara ch){
		for ( int i = 0;i < 5;i++ ) {
			System.out.println(ch.name+"は攻撃しました。");
		}
	}
}
実行結果

魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
魔法使いの少女は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。
大剣使いの少年は攻撃しました。

Threadクラスのjoinメソッドで同期する

joinメソッドは対象スレッドオブジェクトの処理が終了するまで待機します。

まずは、適当に作ったプログラムで同期が使えそうな例を見てみましょう。

キャラ同士の戦闘を別スレッドに任せ、戦闘終了後に、終了と表示して、プログラムを表示させたいとします。

public class Test{
	public static void main(String[] args){
		Chara boy = new Chara("大剣使いの少年");
		Chara girl = new Chara("魔法使いの少女");
		boy.start();
		girl.start();
		System.out.println("終了");
	}
}
class Chara extends Thread{
	String name;//キャラクタの名前。
	Chara(String name){
		this.name = name;
	}
	public void run(){
		Battle.process(this);
	}
}
class Battle{
	synchronized static void process(Chara ch){
		for ( int i = 0;i < 5;i++ ) {
			System.out.println(ch.name+"は戦闘中です。");
		}
	}
}
実行結果

終了
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。

終了が先に表示されてしまいました。

プログラムをjoinメソッドを使用するように修正してみます。

public class Test{
	public static void main(String[] args)throws InterruptedException{
		Chara boy = new Chara("大剣使いの少年");
		Chara girl = new Chara("魔法使いの少女");
		boy.start();
		girl.start();
		boy.join();//boyスレッドが終了するまで待機
		girl.join();//girlスレッドが終了するまで待機
		System.out.println("終了");
	}
}
class Chara extends Thread{
	String name;//キャラクタの名前。
	Chara(String name){
		this.name = name;
	}
	public void run(){
		Battle.process(this);
	}
}
class Battle{
	synchronized static void process(Chara ch){
		for ( int i = 0;i < 5;i++ ) {
			System.out.println(ch.name+"は戦闘中です。");
		}
	}
}
実行結果

大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
大剣使いの少年は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
魔法使いの少女は戦闘中です。
終了

このようにjoinメソッドを利用することで、別スレッドの終了まで待機することができます。

Java

Posted by nompor