【Java】マルチスレッド処理:「synchronized」を使用して同期処理を行う

■スレッド

スレッドとは、「道筋」や「糸」のような意味である。

プログラムの処理を実行する際、1度に1つの命令を実行していく処理をシングルスレッドと呼ぶ。
一方、1度に2つ以上の命令を同時に実行する処理をマルチスレッドと呼ぶ。

マルチスレッド処理で「synchronized」を付けたメソッドを呼び出しても、1度に1つのすれスレッドのみ実行可能となる。(処理が終わるまで、他のスレッドは順番待ちをする)

■作成するプログラムの概要

・ユーザクラスを100個インスタンスする。

・各ユーザクラスにて、カウンタクラスのカウント処理(+1する)を1000回実行する。
なお、カウントする対象は、各クラスで共通のものとする。
※カウンタの合計値は、100(ユーザクラス)×1000(カウント処理)=100000回となる想定。

・「join」メソッドを使用し、すべてのユーザクラスの処理が終了した後、最終的な合計値(100000)を出力する。

■フローチャート

▼カウントクラス

・カウントアップ処理(同期処理)

▼ユーザクラス(Threadを継承)

・runメソッド(オーバーライド)

▼メインクラス

■プログラム仕様

▼カウントクラス

・カウントアップ処理(同期処理)

処理名カウントアップ処理(同期処理)
処理概要カウンタを+1する。
※カウンタは「カウントクラス」にて定義(グローバル)
引数なし
戻り値なし
入力処理内容出力
カウンタを+1する。

▼ユーザクラス(Threadを継承)

・runメソッド(オーバーライド)

処理名runメソッド(オーバーライド)
処理概要「カウントクラス」の「カウントアップ処理」を1000回繰り返す。
引数なし
戻り値なし
入力処理内容出力
■ループ処理:1000回繰り返す。
|「カウントクラス」の「カウントアップ処理」実行。

▼メインクラス

処理名メインクラス
処理概要・100個のユーザクラスをインスタンスする。
・各ユーザクラスについて、スレッド処理を実行する。
・最終的なカウンタの合計値を出力する。
引数なし
戻り値なし
入力処理内容出力

ユーザクラスを100個インスタンス

■ループ処理:100回繰り返す。
|各ユーザクラスをインスタンスする。
|各ユーザクラスでスレッド処理を実行

■ループ処理:100回繰り返す。
|[try]
||各ユーザクラスの処理が終了するのを待つ。
|[catch]
||例外エラーを出力する。

【コンソール】
例外エラー

最終的なカウンタの合計数を出力する。

【コンソール】
カウンタの合計値

■サンプルコード

// カウントクラス
class Counter{
	// 初期値0を設定
	public static int count = 0;
	
	// カウントアップ処理
	public static synchronized void CountUp(){
		count = count + 1;
	}
}

// ユーザクラス(スレッドを継承)
class User extends Thread{
	// runメソッドをオーバーライド
	public void run() {
		// 1000回処理繰り返す
		for(int i = 0; i < 1000; i++) {
			// カウントクラスのカウントアップ処理を実行
			Counter.CountUp();
		}
	}
}

// メインクラス
public class MultiThreadSync {
	public static void main(String[] args) {
		// ユーザクラスを100個インスタンス
		User[] users = new User[100];
		
		// 100個のインスタンス分繰り返す
		for(int i = 0; i < 100; i++) {
			// 各ユーザクラスをインスタンス
			users[i] = new User();
			// 各ユーザクラスでスレッド処理実行
			users[i].start();
		}
		
		// 100回繰り返す
		for(int j = 0; j < 100; j++) {
			try{
				users[j].join();
			}catch(Exception e){
				System.out.println(e);
			}
		}
		// 最終的に何回加算されたか出力する
		System.out.println(Counter.count);
	}
}

■実行結果

・本処理を5回実行した場合、合計値はすべて「100000」である。

100000
100000
100000
100000
100000

■補足:非同期処理の実行結果

「カウンタクラス」の「カウントアップ処理」について、「synchronized」を付与せずに非同期処理にて、5回実行すると以下のようになる。

96873
98045
99293
99962
100000

上記のような結果となる理由として、非同期の場合、ユーザAのカウントアップ処理時にユーザBの処理が割り込みが発生するためである。
※割り込みが発生した場合、共通のカウンタの値を参照し、同じカウンタの値(例:6)を取得し、加算(+1)してカウンタ値(例:7)としてしまうため。

■参考:同期、非同期処理イメージ

(例)2つユーザが共通のカウンタをそれぞれ100回加算(+1)する処理
※非同期処理は1回だけ、ユーザA、Bの処理が同時に発生したものとする。

同期処理の場合、ユーザAが加算処理を行なっている間は、ユーザBの加算処理は実行されない。
したがって、ユーザA、Bの処理が同時に実行されることはないため、共通のカウンタを使用しても、合計カウンタ値は、200となる。

非同期処理の場合、ユーザAとユーザBの加算処理が同時に発生する場合がある。
したがって、上図の通り、ユーザAの3回目の処理と、ユーザBの1回目の処理が同時に実行される。
同時に実行された場合、ユーザA、Bは、同じカウンタの値「3」を参照し、加算処理(+1)を行うことで、「4」の結果が出力されてしまう。
結果として、1度同じ値について加算処理を行なったため、最終的な合計値は「199」となる。

コメント