第三章 (2): 公開と逸出

昨日の朝は勉強出来なかったので補完。今日も一人Java並行処理読書会です。

内部クラスを公開するのは危険

内部クラスのインスタンスは暗黙的にエンクロージングインスタンスへの参照を持つため、これを公開するのは危険とのこと。うーん、何が危険なんだろう・・・。
例えば以下のようにHelloImplを公開するとする。

public class EnclosingSample {

	public static void main(String[] args) {

		Enclosing enclosing = new Enclosing(1);

		Hello hello1 = enclosing.getHello();
		hello1.sayHello();
		System.out.println(hello1.toString());

		Hello hello2 = enclosing.new HelloImpl(2);
		hello2.sayHello();
		System.out.println(hello2.toString());

	}

	private static interface Hello {
		void sayHello();
	}

	private static class Enclosing {

		private final String name;

		private final Hello hello;

		public Enclosing(int seqNo) {
			this.name = "enclosingInstance" + seqNo;
			this.hello = new HelloImpl(seqNo);
		}

		public Hello getHello() {
			return hello;
		}

		public class HelloImpl implements Hello {
			private final String name;

			HelloImpl(int seqNo) {
				this.name = "innerInstance" + seqNo;
			}

			public void sayHello() {
				System.out.println("Hello, " + name);
			}

			@Override
			public String toString() {
				return Enclosing.HelloImpl.this.name + " of " + Enclosing.this.name;
			}
		}

	}

}

実行結果。

Hello, innerInstance1
innerInstance1 of enclosingInstance1
Hello, innerInstance2
innerInstance2 of enclosingInstance1

ここでenclosingInstanceの名前が見えてしまっているのが問題ということ?でもそれは意図しているわけで危険ではない・・・と思う。何でだろうと思ってちょっと考えたのだけど、リフレクションによってアクセスされる可能性があるということに気付きました。

try {
	Field enclosingField = hello2.getClass().getEnclosingClass().getDeclaredField("name");
	try {
		enclosingField.setAccessible(true);
		String enclosingName = (String) enclosingField.get(enclosing);
		System.out.println("get enclosingInstance name: " + enclosingName);
	} catch (IllegalArgumentException e) {
		// ignore
	} catch (IllegalAccessException e) {
		// ignore
	} finally {
		enclosingField.setAccessible(false);
	}
} catch (SecurityException e) {
	// ignore
} catch (NoSuchFieldException e) {
	// ignore
}
get enclosingInstance name: enclosingInstance1

リフレクションを使うとエンクロージングインスタンスのprivateな情報にアクセス出来てしまうわけだ。例えばあるインターフェース(今回の場合はHello)を実装した内部クラス(HelloImpl)を外部に公開して、インターフェースを介して別の箇所でその内部クラスへアクセスすることがあると、(そこではエンクロージングクラスは意識されていないにも関わらず)暗黙のthis参照からエンクロージングインスタンスにまで到達出来てしまうんだなぁ。

安全なコンストラクション

コンストラクタの中でスレッドをスタートすると、新たに作られたスレッドには自分を収めるオブジェクトが、そのオブジェクトを完全に構築する前に見えてしまう。ちょっと分かりづらいな。

private static class Unsafe {
	
	private final String name;
	
	Unsafe() {
		
		new Thread() {
			@Override
			public void run() {
				System.out.println(name);
			}
		}.start();
		
		doSomething();
		this.name = "unsafe";
		
	}
	
	private void doSomething() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// ignore
		}
	}
	
}

こんな感じにすると、意図せず null が出力されてしまう。つまりコンストラクタの中で別スレッドを生成・スタートしてしまうと、別スレッドから完全に構築される前のオブジェクトが逸出してしまう。スレッドを作るのは良いけど、スタートは別タイミングで実行した方が良いということ。

スレッド拘束

データが1つのスレッドからしかアクセスされないのであれば、そもそも同期化は不要。

方法1: ローカル変数を使う

例えばコレクションの引数を受け取ったとき、一旦スナップショットをローカル変数に保存してしまう。そうするとローカル変数はそのスレッドに拘束されるので、安全に利用出来る。

private List<Family> build(List<Person> source) {
	List<Person> snapshot = new ArrayList<Person>(source);
	List<Family> family = new ArrayList<Family>();
	for (Person value : snapshot) {
		// doSomething
	}
	return family;
}

方法2: ThreadLocalを使う

ThreadLocalはそのスレッドが初めてgetを呼び出したとき、initialValueを保存する。

private static class ConnectionManager {

	private final ThreadLocal<Connection> connectionHolder;

	public ConnectionManager() {
		connectionHolder = new ThreadLocal<Connection>() {
			@Override
			protected Connection initialValue() {
				try {
					return DriverManager.getConnection(PATH);
				} catch (SQLException e) {
					throw new RuntimeException(e);
				}
			}
		};
	}

	public Connection getConnection() {
		return connectionHolder.get();
	}

}

オブジェクトを安全に公開するには

  1. オブジェクトの参照をstatic initializerで初期化する
  2. オブジェクトの参照を正しく構築されるオブジェクトのfinalフィールドに保存する
  3. オブジェクトの参照をvolatileフィールドもしくはAtomicReferenceに保存する
  4. オブジェクトの参照をロックによって正しくガードされたフィールドに保存する

1, 2は結局イミュータブルなオブジェクトが安全だよ、ということ。但し上に書いたようにコンストラクタでスレッドをスタートするなどの安全ではない方法で不変オブジェクトを構築してはいけない。3, 4は可変オブジェクトを如何に調停するか。