synchronized ブロック

Revised: Sep./01st/2004; since: Mar./23rd/2002

前節で synchronized 修飾子を紹介しました。これはメンバーをロックして多重アクセスを防ぐものです。同様の発想で、制御の特定の部分をブロック化して、その部分だけロックを掛けることも出来ます。このようなブロックを synchronized ブロックと呼びます。

サンプル

シングルスレッドの例

クラス Customer のメソッド addCust() は、引数で受け取った文字列 name が、顧客名簿 CustList 型オブジェクト list に存在するかどうか調べて list.isCust(name) 、存在すれば true を返し、存在しなければ名簿に追加 list.addCust(name) して false を返します。

import java.io.*;

// 顧客名簿オブジェクト
class CustList {
	// 顧客名簿ファイルの抽象パス名オブジェクト
	private File list = new File("customers.txt");

	// 顧客名簿に名前が存在するかどうかチェック
	public boolean isCust(String name) {
		try {
			// 入力ストリームのバッファリング
			BufferedReader br = new BufferedReader(new FileReader(list));

			// 一行読み込んで str に代入し、
			// ファイル終了まで繰り返す。
			String str = "";
			while ((str = br.readLine()) != null) {
				if (str.indexOf(name) != -1) {
					// 出力ストリームのクローズ
					br.close();
					// 名前が見付かれば true を返す
					return true;
				}
			}
			// 出力ストリームのクローズ
			br.close();
		} catch (Exception e) {
			System.out.println(e);
		}
		// ファイル終了まで名前が見付からなかった場合に、
		// false を返す。
		return false;
	}

	// 顧客名簿への書き出し
	public void addCust(String name) {
		try {
			// 出力ストリームのバッファリング
			BufferedWriter bw = new BufferedWriter(new FileWriter("customers.txt", true));
			bw.write(name);
			bw.newLine();
			bw.flush();
			bw.close();
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}


class Customer {
	// 顧客名簿ファイルの抽象パス名オブジェクト
	CustList list;

	// コンストラクタ
	Customer(CustList obj) {
		list = obj;
	}

	boolean addCust(String name) {
		if (list.isCust(name)) {
			return false;
		} else {
			// 作為的に矛盾を起こす為の sleep
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				System.out.println(e);
			}
			list.addCust(name);
		}
		return true;
	}
}

addCust() では、list.isCust(name)false になるとき(顧客名が見つからなかったとき)、最低でも2秒間停止するように sleep() させています。2秒を超えると、CPUのディスパッチの待機状態になります。シングルスレッドでは単に停止するだけですが、マルチスレッドの場合、ここで停止している間に他のスレッドが追加してしまうなどの不整合が発生するタイミングを広げることになります。

このクラスを使うコントロール・クラスは次のようにかけます:

CustTest.java

class CustTest {
	public static void main(String[] args) {
		String name="";
		for (int i = 0; i < args.length; i++) {
			name = name + " " + args[i];
		}
		CustList list = new CustList();
		Customer cust = new Customer(list);
		System.out.println(cust.addCust(name));
	}
}

コマンドライン:

C:\Java\Thread>javac CustTest.java

C:\Java\Thread>java CustTest sugai
true

C:\Java\Thread>java CustTest sugai manabu
true

C:\Java\Thread>java CustTest 菅井 学
true

C:\Java\Thread>java CustTest sugai
false

C:\Java\Thread>

上の実行例では、 customers.txt は次のようになります:

 sugai
 sugai manabu
 菅井 学

同期指定しないマルチスレッドの例

これをマルチ・スレッドで走らせるコントロール・クラスを次のように書いたとします:

CustTest2.java

class CustName implements Runnable {
	Customer cust;
	String name;

	CustName(Customer obj, String str) {
		cust = obj;
		name = str;
	}

	public void run() {
		System.out.println(cust.addCust(name));
	}
}

class CustTest2 {
	public static void main(String[] args) {
		CustList list = new CustList();
		Customer customer = new Customer(list);
		CustName[] cust = new CustName[5];

		cust[0] = new CustName(customer, "sugai");
		cust[1] = new CustName(customer, "suzuki");
		cust[2] = new CustName(customer, "sugai");
		cust[3] = new CustName(customer, "sato");
		cust[4] = new CustName(customer, "sugai");

		// スレッドの配列の宣言
		Thread[] thres = new Thread[5];

		// 5つのスレッドの作成
		for (int i=0; i<5; i++) {
			// Runnable オブジェクトをスレッドに委譲
			thres[i] = new Thread(cust[i]);
			// スレッドの開始
			thres[i].start();
		}
	}
}
C:\Java\Thread>javac CustTest2.java

C:\Java\Thread>java CustTest2
true
true
true
true
true

C:\Java\Thread>

この結果、 customers.txt は次のようになります:

sugai
suzuki
sato
sugai
sugai

同じ名前のエントリーが複数作成されています。この矛盾は、一つのスレッドが書き込む前に、次のスレッドが読み込んでサーチしてしまうために、エントリーがないものと思って書き込んでしまうために発生します。

synchronized 修飾子により同期指定したマルチスレッドプログラム

これを回避する為には、前節synchronized 修飾子では次のように修正することになります。

class Customer {
	CustList list;

	Customer(CustList obj) {
		list = obj;
	}

	synchronized boolean addCust(String name) {
		if (list.isCust(name)) {
			return false;
		} else {

			// 作為的に矛盾を起こす為の sleep
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				System.out.println(e);
			}

			list.addCust(name);
		}
		return true;
	}
}

これで次のように矛盾は回避されます:

C:\Java\Thread>javac CustTest2.java

C:\Java\Thread>java CustTest2
true
true
false
true
false

C:\Java\Thread>

結果として、 customers.txt は次のようにまります:

sugai
suzuki
sato

書き込みメソッドが同期指定された為に、重複書き込みがなくなりました。

synchronized ブロックにより同期指定したマルチスレッドプログラム

しかしながら、 Customer クラスで保護したいのは、 addCust() メソッド全てではなく、その一部のコードであるのも事実です。また、同期保護したいのは Customer クラス型オブジェクトではなく、そこで利用する CustList クラス型オブジェクト list の方です。

これらの2点を考慮すると、 Customer クラスは次のように書くことで同期保護できます。

class Customer {
	CustList list;

	Customer(CustList obj) {
		list = obj;
	}

	boolean addCust(String name) {
		synchronized(list) {
			if (list.isCust(name)) {
				return false;
			} else {

				// 作為的に矛盾を起こす為の sleep
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					System.out.println(e);
				}

				list.addCust(name);
			}
		}
		return true;
	}
}

このとき、 customer.txt を白紙にした状態での実行結果は次のようになります。

C:\Java\Thread>javac CustTest2.java

C:\Java\Thread>java CustTest2
true
false
true
false
true

C:\Java\Thread>
sugai
suzuki
sato


Copyright © 2002 SUGAI, Manabu. All Rights Reserved.