マルチスレッド化プログラムのテストおよびデバッグはきわめて困難です。というのは、多くの場合、並行性の障害は現れ方が一様でなかったり、再現しなかったりすることが、しばしばだからです。ほどんどのスレッド化の問題は本質的に予測不可能であり、特定のプラットフォーム (ユニプロセッサー・システムなど) や特定の負荷レベル以下ではまったく発生しません。マルチスレッド化プログラムの正しさのテストは非常に難しく、バグが現れるまでに非常に時間がかかるので、初めからスレッド・セーフティーを考慮したアプリケーションの開発がさらに重要になります。今回の記事では、「構築中にthis 参照を漏らしてしまう」特定のスレッド・セーフティーの問題 (以降、参照の漏洩の問題と呼びます) が、どのように望ましくない結果をもたらすかについて見ていきます。そして、スレッド・セーフのコンストラクターを作成するためのガイドラインを確立します。
スレッド・セーフティーの違反に関してプログラムを分析することは非常に難しい場合があり、専門技術が要求されます。しかし、幸いにも、そしておそらく驚くべきことに、スレッド・セーフのクラスを最初から作成することは、規律という異なる専門技術が必要になりますが、さほど困難ではありません。ほとんどの並行性のエラーは、プログラマーが、便利さ、知覚できるほどのパフォーマンス上の利点、あるいは単なる怠慢のために規則を破ろうとすることが原因で生じます。他の多くの並行性の問題と同様に、コンストラクターを作成する際に2、3の簡単な規則に従うことによって、参照の漏洩の問題を回避することができます。
ほとんどの並行性の問題は、突きつめれば何らかのデータ競合に関係しています。データ競合または競合状態は、複数のスレッドまたはプロセスが共用データ項目を読み込んだり書き込んだりする際に発生し、最終的な結果は、スレッドがスケジューリングされる順序によって決まります。リスト1は、スレッドのスケジューリング次第でプログラムが0または1をプリントする単純なデータ競合の例を示しています。
リスト1. 単純なデータ競合
public class DataRace {
static int a = 0;
public static void main() {
new MyThread().start();
a = 1;
}
public static class MyThread extends Thread {
public void run() { System.out.println(a);
}
}
}
|
2番目のスレッドが直ちにスケジューリングされると、a の初期値0がプリントされます。あるいは、2番目のスレッドがすぐには実行されないと、代わりに値1がプリントされます。このプログラムの出力は、使用しているJDK、基本となるオペレーティング・システムのスケジューラー、または任意のタイミング的要素によって異なります。このプログラムを何度か実行すると、異なる結果が得られるでしょう。
実際には、最初のスレッドがa を1に設定する前に2番目のスレッドの実行が開始されるか、設定した後に開始されるかという明らかな競合以外に、リスト1には別のデータ競合があります。2つめの競合は、スレッド間のデータ変更の可視性を保証する同期を2つのスレッドが使用しないという可視性の競合です。同期がないので、a への代入が最初のスレッドによって完了した後に2番目のスレッドが実行される場合、最初のスレッドによって行われた変更が、直ちに2番目のスレッドに見える場合も、また見えない場合もあります。最初のスレッドがすでにa に値1を代入した場合でも、2番目のスレッドがaを値0と見なすこともあります。適切な同期無しに2つのスレッドが同じ変数にアクセスするこの2番目のデータ競合は複雑な問題ですが、幸いにも、別のスレッドによって最後に書き込まれた変数を読み込む場合や、別のスレッドによって次に読み込まれる変数を書き込む場合に必ず同期を使用することによって、この種のデータ競合を回避することができます。ここでは、この種のデータ競合についてはこれ以上詳しく説明しませんので、サイドバーの「Java Memory Modelによる同期」を参照してください。また、この複雑な問題に関する詳細については、参考文献を参照してください。
データ競合をクラスに取り込むおそれのある誤りの1つは、コンストラクターが完成する前にthis 参照を別のスレッドに公開してしまうことです。静的なフィールドまたは静的なコレクションにthis を直接格納するなど、参照が明示的な場合もありますが、コンストラクターの非静的な内部クラスのインスタンスに参照を公開するなど、参照が暗黙的な場合もあります。コンストラクターは普通のメソッドではありません。コンストラクターには、初期化の安全のための特別なセマンティクスが付随します。コンストラクターが完了した後は、オブジェクトは、予測可能な一貫した状態であることが想定されるので、不完全な構築のオブジェクトに参照を公開することは危険です。リスト2は、このような競合状態をコンストラクターに取り込んでしまう例を示しています。これは、無害であるように見えますが、重大な並行性の問題の種をはらんでいます。
リスト2. 発生を待ちうけるデータ競合
public class EventListener { public EventListener(EventSource eventSource) {
// do our initialization
...
// register ourselves with the event source
eventSource.registerListener(this);
}
public onEvent(Event e) { // handle the event
}
}
|
一見したところでは、EventListener クラスは無害であるように見えます。他のスレッドが見ることのできる新しいオブジェクトに参照を公開するリスナーの登録は、コンストラクターの一番最後で行われます。しかし、スレッド間の可視性の違いやメモリー・アクセスの再配列などのJava Memory Model (JMM) の問題をすべて無視しても、このコードは依然として、不完全な構築のEventListener オブジェクトを他のスレッドに公開する危険性があります。リスト3のように、EventListener をサブクラス化するとどうなるか考えてみましょう。
リスト3. EventListenerをサブクラス化した場合に起こる問題
public class RecordingEventListener extends EventListener {
private final ArrayList list;
public RecordingEventListener(EventSource eventSource) {
super(eventSource);
list = Collections.synchronizedList(new ArrayList());
}
public onEvent(Event e) { list.add(e);
super.onEvent(e);
}
public Event[] getEvents() {
return (Event[]) list.toArray(new Event[0]);
}
}
|
Java言語仕様では、super() の呼び出しがサブクラス・コンストラクターの最初のステートメントであることが求められるので、サブクラス・フィールドの初期化を終える前に、未構築のイベント・リスナーがイベント・ソースに既に登録されています。ここで、list フィールドにデータ競合があります。イベント・リスナーが登録呼び出し内からイベントを送信する場合や、または単に運悪くイベントがこのよくない瞬間に到着した場合、null のデフォルト値が依然としてlist にある間にRecordingEventListener.onEvent() が呼び出され、NullPointerException 例外をスローします。onEvent() などのクラス・メソッドが、まだ初期化されていない最終フィールドに対してコーディングを行なわれるべきではありません。
リスト2 の問題は、構築が完了する前にEventListener が、構築中のオブジェクトに参照を公開してしまったことです。オブジェクトはほぼ完全に構築されているように見え、したがってthis をイベント・ソースに渡すことは安全に見えましたが、見た目に惑わされることもあります。リスト2のようにコンストラクター内からthis 参照を公開することは、爆発を待つ時限爆弾のようなものです。
this 参照をまったく使用しなくても、参照の漏洩の問題が発生する場合があります。非静的な内部クラスは、その親オブジェクトのthis 参照の暗黙的なコピーを維持するので、匿名の内部クラス・インスタンスを作成し、それを現在のスレッド外から見えるオブジェクトに渡すことは、this 参照自体を公開するのとまったく同じリスクがあります。リスト4にはリスト2と同じ基本的な問題がありますが、this 参照を明示的に使用していません。
リスト4. 匿名の内部クラスを使用して「this」を不適切に公開する
public class EventListener2 {
public EventListener2(EventSource eventSource) {
eventSource.registerListener(
new EventListener() {
public void onEvent(Event e) { eventReceived(e);
}
});
}
public void eventReceived(Event e) {
}
}
|
EventListener2 クラスには、構築中のオブジェクトの参照が、別のスレッドから見える場所に公開されるというリスト2 のEventListener と同じ問題があります (この場合は間接的です)。EventListener2 をサブクラス化すると、サブクラス・コンストラクターが完成する前にサブクラス・メソッドが呼び出されるという同じ問題が起こります。
リスト4 の問題の特別なケースは、コンストラクター内からスレッドを開始することです。というのは、オブジェクトがスレッドを所有する場合、多くは、そのスレッドは内部クラスであるか、またはthis 参照をそのコンストラクターに渡す (あるいはクラス自体がThread クラスを漏洩する)からです。オブジェクトがスレッドを所有する場合、ちょうどThread が行うようにオブジェクトがstart() メソッドを提供し、コンストラクターからではなくstart() メソッドからスレッドを開始するのが最良です。それによって、クラスの実装の詳細 (所有されるスレッドの存在の可能性など) がインターフェースを介してある程度公開されます。これは、多くの場合望ましくありませんが、この場合、実装を隠すことによる利点よりも、スレッドをコンストラクターから開始するリスクの方が大きいのです。
構築中のthis 参照への参照がすべて有害というわけではありません。害があるのは、他のスレッドから見える参照を公開する参照だけです。this 参照を別のオブジェクトと共用することが安全かどうかを判断するには、そのオブジェクトの可視性と、そのオブジェクトが参照によって何を行うのかを深く理解することが必要です。リスト5は、構築中のthis 参照の漏洩に関して安全な実践例と危険な実践例を示しています。
リスト5. 構築中の「this」参照の安全な使用と危険な使用
public class Safe { private Object me;
private Set set = new HashSet();
private Thread thread;
public Safe() { // Safe because "me" is not visible from any other thread
me = this;
// Safe because "set" is not visible from any other thread
set.add(this);
// Safe because MyThread won't start until construction is complete
// and the constructor doesn't publish the reference
thread = new MyThread(this);
}
public void start() {
thread.start();
}
private class MyThread(Object o) {
private Object theObject;
public MyThread(Object o) { this.theObject = o;
}
...
}
}
public class Unsafe {
public static Unsafe anInstance;
public static Set set = new HashSet();
private Set mySet = new HashSet();
public Unsafe() {
// Unsafe because anInstance is globally visible
anInstance = this;
// Unsafe because SomeOtherClass.anInstance is globally visible
SomeOtherClass.anInstance = this;
// Unsafe because SomeOtherClass might save the "this" reference
// where another thread could see it
SomeOtherClass.registerObject(this);
// Unsafe because set is globally visible set.add(this);
// Unsafe because we are publishing a reference to mySet
mySet.add(this);
SomeOtherClass.someMethod(mySet);
// Unsafe because the "this" object will be visible from the new
// thread before the constructor completes
thread = new MyThread(this);
thread.start();
}
public Unsafe(Collection c) {
// Unsafe because "c" may be visible from other threads
c.add(this);
}
}
|
ご覧のとおり、Unsafe クラスの危険な構成の多くは、Safe クラスの安全な構成に非常によく似ています。this 参照が別のスレッドから見えるようになる恐れがあるかどうかは、判断が難しい場合があります。最良の戦略は、コンストラクターでthis 参照を使うことを完全に (直接的にも間接的にも) 回避することです。しかし現実には、これが常に可能とは限りません。this 参照と、コンストラクターでの非静的な内部クラスのインスタンスの作成には、十分な注意が必要です。
スレッド・セーフの構築に関して上述した実践は、同期の効果について考えた場合にさらにその重要性を増します。たとえば、スレッドAがスレッドBを開始した場合、Java Language Specification (JLS) は、スレッドAがスレッドBを開始した時点でスレッドAに見えていたすべての変数がスレッドBから見えることを保証しています。これは、事実上Thread.start() に暗黙的な同期を持っているようなものです。コンストラクター内からスレッドを開始すると、構築中のオブジェクトは構築が完了していないので、このような可視性の保証がなくなってしまいます。
さらにややこしいいくつかの問題に対応するために、JMMはJava Community Process JSR 133の下で修正中です。それは、(特に)volatile とfinal のセマンティクスを一般的な直観にさらに合うように変更します。たとえば、現在のJMMセマンティクスの下では、スレッドは常にfinal フィールドに複数の値を持つ可能性があります。新しいメモリー・モデルのセマンティクスはこれを防ぎますが、それはコンストラクターが適切に定義されている場合だけです。つまり、構築中にthis 参照は漏洩されない場合です。
別のスレッドから見える構築が不完全なオブジェクトへの参照を作ってしまうことが望ましくないのは明らかです。それでは、適切に構築されたオブジェクトと不完全なオブジェクトを見分けるにはどうしたらよいでしょうか。直接的にでも、または内部クラスを介して間接的にでも、コンストラクターの内部からthis の参照を公開した場合、それを見分けることができ、予測不可能な結果を招くことになります。この問題を防ぐために、this の使用、内部クラスのインスタンスの作成、そしてコンストラクターからのスレッドの開始は避けるようにしてください。this の直接的な使用、またはコンストラクターでの間接的な使用を避けることができない場合は、this 参照が他のスレッドから見えないようにしてください。
- Doug Lea氏の 「Concurrent Programming in Java, Second Edition」(Addison-Wesley、1999年) は、Javaアプリケーションのマルチスレッド化プログラミングを取り巻く微妙な問題に関する優れた書籍です。
-
Synchronization and the Java Memory Model は、
synchronizedの実際の意味について取り上げたDoug Lea氏の書籍からの引用です。 - 「Double-checked locking: Clever, but broken」 (JavaWorld、2001年2月) および「Can double-checked locking be fixed?」 (JavaWorld、2001年5月) は、JMMについて取り上げ、特定の状況で同期を怠った場合の思いがけない結果について説明しています。
- 「double-checked lockingとSingletonパターン」 (developerWorks、2002年5月) でPeter Haggar氏は、同期を怠った場合にいかに奇妙なことが起こるかを段階的に説明しています。
-
Semantics of Multithreaded Java (PDF) は、JSR133の結果としてJava Memory Modelで提案されている変更について詳しく説明しています。
- 「マルチスレッド化Javaアプリケーションの作成」 (developerWorks、2001年2月) でAlex Roetter氏は、Javaクラスのスレッド、同期、およびロックの基本的な概要を説明しています。
- Brian Goetz氏の 「Javaの理論と実践」のコラムをお読みください。
- developerWorksJava technologyゾーンで他のJava technologyコンテンツをご覧ください。
Brian Goetz は18 年間以上に渡って、専門的ソフトウェア開発者として働いています。彼はカリフォルニア州ロスアルトスにあるソフトウェア開発コンサルティング会社、Quiotixの主席コンサルタントであり、またいくつかのJCP Expert Groupの一員でもあります。2005年の末にはAddison-Wesleyから、Brianによる著、Java Concurrency In Practiceが出版される予定です。Brian著による有力業界紙に掲載済みおよび掲載予定の記事のリストを参照してください。