タイトル
TOPJavaTIPS → This Page

Synchronized と Singleton での注意点【Java の TIPS、小ネタ、注意点】

前提

Java開発におけるSynchronizedSingletonを使う際の注意点を紹介します。
このページに記載している内容は2011/02/02に書かれたものです。
掲載している画面や方法が将来的に変更されている場合があります。

時々見かける Singleton パターンの例

メジャーなデザインパターンの1つである Singleton パターン。
そんな Singleton パターンで時々見かける実装をまずは紹介。
しかし以下の実装例は問題を含んでいる場合がある。
(どのような問題を含んでいるかは後で紹介)

jp.mitchy_world.synchro_single.SingletonA.java
package jp.mitchy_world.synchro_single;

/**
 * Singletonパターンサンプル(問題ありバージョン)
 * 
 * @author みっちー
 */
public class SingletonA {

	/** Singleton用インスタンス */
	private static SingletonA instance;

	/**
	 * インスタンス取得
	 * 
	 * @param tid
	 *            確認用スレッドID
	 * @return インスタンス
	 */
	public synchronized static SingletonA getInstance(String tid) {
		System.out.println("getInstance[" + tid + "]-S");

		if (instance == null) {
			instance = new SingletonA();
		}

		System.out.println("getInstance[" + tid + "]-E");

		return instance;
	}

	/**
	 * 何らかの処理
	 * 
	 * @param tid
	 *            確認用スレッドID
	 */
	public void execute(String tid) {
		System.out.println("execute[" + tid + "]-S");
		try {
			// 何らかの処理
			Thread.sleep(100);
		} catch (Exception e) {
		}
		System.out.println("execute[" + tid + "]-E");
	}
}

tid つけてるのは後で解説するのであまり気にしないで下さい。

んで、上記クラスを呼び出す際に以下のようにしているのを見かける。
SingletonA.getInstance("xxx").execute("xxx");

上記 Singleton パターンの問題点

getInstance を呼び出すたびに synchronized で同期をとってるからコストがかかるって問題もあるけど
ここで挙げたい問題点はもっと別の問題点なので割愛。

以下、本題。

わざわざ getInstance に synchronized をつけてるってことは複数スレッドから呼び出されても
ちゃんと同期(排他)されて処理が意図した順番に動いて欲しいからだと思う。

でも、上記サンプルだと getInstance メソッドに synchronized はつけているけど
execute メソッドには synchronized がついていない。

すると
SingletonA.getInstance("xxx").execute("xxx");
が複数スレッドから呼び出されると問題が起こる場合がある。
と、いうか上記サンプルは同期(排他)されるのはあくまでも getInstance メソッドだけで、execute メソッドは同期(排他)されない。
しかし、実際には getInstance メソッドにしか synchronized を付けていない実装を見かける。

百聞は一見にしかず。
複数スレッドで呼び出す例を挙げる。

jp.mitchy_world.synchro_single.ThreadA.java
package jp.mitchy_world.synchro_single;

/**
 * 検証用メインクラス(問題ありバージョン)
 * 
 * @author みっちー
 */
public class ThreadA extends Thread {

	/**
	 * スレッド開始
	 */
	public void run() {
		String tid = Thread.currentThread().getName();
		System.out.println("run[" + tid + "]-S");
		SingletonA.getInstance(tid).execute(tid);
		System.out.println("run[" + tid + "]-E");
	}

	/**
	 * メイン
	 * 
	 * @param args
	 *            引数
	 */
	public static void main(String args[]) {
		System.out.println("main-S");

		int count = 5;
		ThreadA thread[] = new ThreadA[count];

		// スレッド開始
		for (int i = 0; i < count; i++) {
			thread[i] = new ThreadA();
			thread[i].start();
		}

		System.out.println("main-E");
	}
}

SingletonA クラスに tid をつけていたのはどのような順番でメソッドが呼び出されるかを
分かりやすくするためです。

さっそく実行してみましょう。

実行するとコンソールに以下のような結果が表示されたと思います。
実際にはスレッドの順番はどういう順番で実行されるかは毎回変わるので
完全に以下と一致はしないと思います。

main-S
run[Thread-0]-S
main-E
getInstance[Thread-0]-S
getInstance[Thread-0]-E
execute[Thread-0]-S
run[Thread-4]-S
getInstance[Thread-4]-S
getInstance[Thread-4]-E
execute[Thread-4]-S
run[Thread-3]-S
getInstance[Thread-3]-S
getInstance[Thread-3]-E
execute[Thread-3]-S
run[Thread-2]-S
getInstance[Thread-2]-S
getInstance[Thread-2]-E
execute[Thread-2]-S
run[Thread-1]-S
getInstance[Thread-1]-S
getInstance[Thread-1]-E
execute[Thread-1]-S
execute[Thread-4]-E
run[Thread-4]-E
execute[Thread-3]-E
execute[Thread-0]-E
run[Thread-3]-E
run[Thread-0]-E
execute[Thread-2]-E
run[Thread-2]-E
execute[Thread-1]-E
run[Thread-1]-E

ここで注目すべきは execute メソッドの実行順序です。
execute メソッドがちゃんと同期(排他)されていれば
execute[Thread-1]-S
execute[Thread-1]-E
execute[Thread-3]-S
execute[Thread-3]-E
のように、特定のスレッド番号で開始(S)されれば、同じスレッド番号で終了(E)するまでは別のスレッド番号で呼び出されないはずです。
しかし、
execute[Thread-0]-S
execute[Thread-4]-S
execute[Thread-3]-S
・・・
のように、終了するまえに別のスレッド番号の開始が呼び出されています。
これは完全に同期(排他)されていません。

もちろん同期(排他)が必要ないメソッドであれば問題ではない。

上記 Singleton パターンの問題点を解決した例


jp.mitchy_world.synchro_single.SingletonB.java
package jp.mitchy_world.synchro_single;

/**
 * Singletonパターンサンプル(問題なしバージョン)
 * 
 * @author みっちー
 */
public class SingletonB {

	/** Singleton用インスタンス */
	private static SingletonB instance;

	/**
	 * インスタンス取得
	 * 
	 * @param tid
	 *            確認用スレッドID
	 * @return インスタンス
	 */
	public synchronized static SingletonB getInstance(String tid) {
		System.out.println("getInstance[" + tid + "]-S");

		if (instance == null) {
			instance = new SingletonB();
		}

		System.out.println("getInstance[" + tid + "]-E");

		return instance;
	}

	/**
	 * 何らかの処理
	 * 
	 * @param tid
	 *            確認用スレッドID
	 */
	public synchronized void execute(String tid) {
		System.out.println("execute[" + tid + "]-S");
		try {
			Thread.sleep(100);
		} catch (Exception e) {
		}
		System.out.println("execute[" + tid + "]-E");
	}
}

jp.mitchy_world.synchro_single.ThreadB.java
package jp.mitchy_world.synchro_single;

/**
 * 検証用メインクラス(問題なしバージョン)
 * 
 * @author みっちー
 */
public class ThreadB extends Thread {

	/**
	 * スレッド開始
	 */
	public void run() {
		String tid = Thread.currentThread().getName();
		System.out.println("run[" + tid + "]-S");
		SingletonB.getInstance(tid).execute(tid);
		System.out.println("run[" + tid + "]-E");
	}

	/**
	 * メイン
	 * 
	 * @param args
	 *            引数
	 */
	public static void main(String args[]) {
		System.out.println("main-S");

		int count = 5;
		ThreadB thread[] = new ThreadB[count];

		// スレッド開始
		for (int i = 0; i < count; i++) {
			thread[i] = new ThreadB();
			thread[i].start();
		}

		System.out.println("main-E");
	}
}

要は execute メソッドに synchronized 付けただけです(^^;;
しかし、実行してみると
execute[Thread-1]-S
execute[Thread-1]-E
execute[Thread-3]-S
execute[Thread-3]-E
execute[Thread-2]-S
execute[Thread-2]-E
のように、開始→終了、開始→終了、開始→終了・・・と同期(排他)されていることが確認できると思います。


ダウンロード

解説で使ったクラスなどを含んだ eclipse 用プロジェクト一式

更新履歴

2011/02/02 新規作成


TOPJavaTIPS → This Page