Javaの理論と実践: 安全な構築のテクニック

構築中に「this」参照を漏洩させない

Java言語は、マルチスレッド化をアプリケーションに容易に組み込むことのできる、柔軟で見たところシンプルなスレッド化機能を提供します。しかし、Javaアプリケーションのコンカレント・プログラミングは見た目よりも複雑です。Javaプログラムでデータ競合や他の並行性の問題を引き起こす難解な (また、さほど難解でもない) いくつかの道筋があります。Javaの理論と実践の今回の記事でBrian Goetz氏は、「構築中にthis 参照を漏らしてしまう」一般的なスレッド化の問題について取り上げています。無害に見えるこの行動は、予測不可能で望ましくない結果をJavaプログラムにもたらす可能性があります。

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix

Brian Goetz は18 年間以上に渡って、専門的ソフトウェア開発者として働いています。彼はカリフォルニア州ロスアルトスにあるソフトウェア開発コンサルティング会社、Quiotixの主席コンサルタントであり、またいくつかのJCP Expert Groupの一員でもあります。2005年の末にはAddison-Wesleyから、Brianによる著、Java Concurrency In Practiceが出版される予定です。Brian著による有力業界紙に掲載済みおよび掲載予定の記事のリストを参照してください。



2002年 6月 01日

マルチスレッド化プログラムのテストおよびデバッグはきわめて困難です。というのは、多くの場合、並行性の障害は現れ方が一様でなかったり、再現しなかったりすることが、しばしばだからです。ほどんどのスレッド化の問題は本質的に予測不可能であり、特定のプラットフォーム (ユニプロセッサー・システムなど) や特定の負荷レベル以下ではまったく発生しません。マルチスレッド化プログラムの正しさのテストは非常に難しく、バグが現れるまでに非常に時間がかかるので、初めからスレッド・セーフティーを考慮したアプリケーションの開発がさらに重要になります。今回の記事では、「構築中に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による同期」を参照してください。また、この複雑な問題に関する詳細については、参考文献を参照してください。


構築中に「this」参照を公開しない

Java Memory Modelによる同期

Javaプログラミングのsynchronized キーワードは、相互排他を強制します。これによって、ある時点であるコードのブロックを実行するスレッドは1つだけであることが保証されます。しかし同期によって、あるいは同期がないことによって、強力なメモリー・モデルのないマルチプロセッサー・システム (つまり、必ずしもキャッシュ一貫性を提供しないプラットフォーム) にさらに微妙な他の結果も生じます。同期によって、あるスレッドの行った変更が、予測可能な方法で他のスレッドから見えるようになります。また、いくつかのアーキテクチャーでは、同期がないと、異なるスレッドから、メモリー操作が実際とは異なる順序で実行されたように見えます。これは紛らわしいことですが、ごく普通のことであり、このようなプラットフォームで優れたパフォーマンスを得る上で重要なことです。別のスレッドによって書き込まれた変数を読み込むたびに、あるいは別のスレッドによって次に読み込まれる変数を書き込むたびに同期を行うという規則に従うだけで、問題は起こりません。詳細については、参考文献を参照してください。

データ競合をクラスに取り込むおそれのある誤りの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 参照の暗黙的なコピーを維持するので、匿名の内部クラス・インスタンスを作成し、それを現在のスレッド外から見えるオブジェクトに渡すことは、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 クラスには、構築中のオブジェクトの参照が、別のスレッドから見える場所に公開されるというリスト2EventListener と同じ問題があります (この場合は間接的です)。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の下で修正中です。それは、(特に)volatilefinal のセマンティクスを一般的な直観にさらに合うように変更します。たとえば、現在のJMMセマンティクスの下では、スレッドは常にfinal フィールドに複数の値を持つ可能性があります。新しいメモリー・モデルのセマンティクスはこれを防ぎますが、それはコンストラクターが適切に定義されている場合だけです。つまり、構築中にthis 参照は漏洩されない場合です。


結論

別のスレッドから見える構築が不完全なオブジェクトへの参照を作ってしまうことが望ましくないのは明らかです。それでは、適切に構築されたオブジェクトと不完全なオブジェクトを見分けるにはどうしたらよいでしょうか。直接的にでも、または内部クラスを介して間接的にでも、コンストラクターの内部からthis の参照を公開した場合、それを見分けることができ、予測不可能な結果を招くことになります。この問題を防ぐために、this の使用、内部クラスのインスタンスの作成、そしてコンストラクターからのスレッドの開始は避けるようにしてください。this の直接的な使用、またはコンストラクターでの間接的な使用を避けることができない場合は、this 参照が他のスレッドから見えないようにしてください。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=224157
ArticleTitle=Javaの理論と実践: 安全な構築のテクニック
publish-date=06012002