JPCERT コーディネーションセンター

TSM03-J. 初期化が完了していないオブジェクトを公開しない

複数のスレッドに共有されるオブジェクトの初期化中は、そのオブジェクトを構築しているスレッドのみがオブジェクトにアクセスできるように制限しなければならない。初期化が完了すれば、そのオブジェクトを公開(他のスレッドから可視な状態)しても安全である。Javaメモリモデル(JMM)では、初期化を開始したが完了はしていないオブジェクトを、複数のスレッドが参照することを許している。したがって適切なコーディングにより、初期化が完了していない状態のオブジェクトが公開されることを防がなければならない。

このルールでは、初期化が完了していないメンバーオブジェクトのインスタンスへの参照を公開することを禁止している。これに対して「TSM01-J. オブジェクトの構築時にthis参照を逸出させない」では、構築中のオブジェクトの this 参照がコンストラクタの外に逸出することを禁止している。 「OBJ11-J. コンストラクタが例外をスローする場合には細心の注意を払う」では、シングルスレッドプログラムであっても、初期化が完了していないオブジェクトを公開すると問題が発生することを説明している。

違反コード

以下の違反コード例では、Foo クラスの initialize() メソッドで Helper オブジェクトを構築している。Helper オブジェクトのフィールドは、Helper クラスのコンストラクタにより初期化されている。

class Foo {
  private Helper helper;

  public Helper getHelper() {
    return helper;
  }

  public void initialize() {
    helper = new Helper(42);
  }
}

public class Helper {
  private int n;

  public Helper(int n) {
    this.n = n;
  }
  // ...
}

あるスレッドが initialize()メソッドの呼出し前に getHelper() メソッドを使用して helper フィールドにアクセスすると、初期化されていない helper フィールドを参照することになるだろう。その後、あるスレッドが initialize() メソッドを、他のスレッドが getHelper() メソッドを呼び出した場合、後者のスレッドは以下のいずれか1つの状態を観測することになる。

Java メモリモデルでは、新しい Helper オブジェクトのためにメモリを割り当て、そのメモリへの参照をオブジェクトの初期化前に helper フィールドに代入することを、コンパイラに許している。つまりコンパイラは、helper インスタンスフィールドへの書込みと、Helper オブジェクトを初期化する書込み(すなわち、this.n = n)について、前者の書き込みを先に行うように順序を変更してもよいのである。それゆえ競合状態が発生して、他のスレッドが初期化途中の Helper オブジェクトのインスタンスを見る可能性がある。

もう一つ別の問題がある。2つ以上のスレッドが initialize() メソッドを呼び出すと、Helper オブジェクトが複数生成されてしまうことである。これは性能上の問題であり、処理結果自体は正しい。生成される各オブジェクトのフィールド n は適切に初期化され、複数生成された Helper オブジェクトのうち未使用のものはガベージコレクションされる。

適合コード (メソッド同期)

メソッド同期を適切に使用することによって、初期化が完了していないオブジェクトの参照の公開を防ぐことができる。これを以下の適合コードに示す。

class Foo {
  private Helper helper;

  public synchronized Helper getHelper() {
    return helper;
  }

  public synchronized void initialize() {
    helper = new Helper(42);
  }
}

両方のメソッドを synchronized 修飾することで、両者が同時に実行されないことを保証している。一方のスレッドが initialize()メソッドを呼んだ直後に、他のスレッドが getHelper() メソッドを呼ぶと、synchronized 修飾された initialize() メソッドは必ず先に終了する。synchronized 修飾子は、2つのスレッド間の事前発生関係を確立する。これにより、getHelper() メソッドを呼び出すスレッドは、初期化が完了した Helper オブジェクトを読み取るか、あるいは何も読み取らない(すなわち helpernull 参照を保持している)かのいずれかであることが保証される。このアプローチを使えば、メンバーフィールドが可変であっても不変であってもオブジェクトを適切に公開することができる。

適合コード (finalフィールド)

Java メモリモデルでは、オブジェクトを構築するコンストラクタの完了前に、スレッドがフィールドにアクセスした場合でも、そのフィールドが final 宣言されていれば、必ず初期化が完了した値が返ることを保証している。

class Foo {
  private final Helper helper;

  public Helper getHelper() {
    return helper;
  }

  public Foo() {
    // Point 1
    helper = new Helper(42);
    // Point 2
  }
}

しかしこの適合コードでは、helper フィールドへの Helper インスタンス割当てを、Foo のコンストラクタで行う必要がある。Java言語仕様§17.5.2「構築中におけるfinalフィールドの読み込み」 [JLS 2015] には次のように書かれている。

オブジェクトの構築を行うスレッド内における、該当オブジェクトの final フィールドの読込みは、通常の事前発生の規則によって、コンストラクタ内の該当フィールドの初期化に従って順序付けられる。コンストラクタ内でフィールドが設定された後に読込みが発生する場合、それは final フィールドに代入された値を観測し、そうでない場合はデフォルト値を観測する。

したがって、Foo クラスのコンストラクタがその処理を完了するまで、Helper インスタンスへの参照を公開してはならない(「TSM01-J. オブジェクトの構築時にthis参照を逸出させない」も参照)。

適合コード (finalフィールドとスレッドセーフなコンポジション)

コレクションクラスの中には、オブジェクトが持っている要素へのスレッドセーフなアクセスを提供するものもある。Helper オブジェクトがそのようなコレクションに格納される場合、そのオブジェクトの参照が可視となる前に初期化が完了することが保証される。以下の適合コードでは、helper フィールドを Vector<Helper> 内に格納している。

class Foo {
  private final Vector<Helper> helper;

  public Foo() {
    helper = new Vector<Helper>();
  }

  public Helper getHelper() {
    if (helper.isEmpty()) {
      initialize();
    }
    return helper.elementAt(0);
  }

  public synchronized void initialize() {
    if (helper.isEmpty()) {
      helper.add(new Helper(42));
    }
  }
}

helper フィールドを final 宣言し、アクセスが発生する前に Vector オブジェクトが構築されることを保証している。helper フィールドは、synchronized 修飾された initialize() メソッドを呼び出すことで安全に初期化することが可能であり、常に1つの Helper オブジェクトだけが Vector オブジェクトに追加されることが保証される。getHelper() メソッドが initialize() メソッドより前に呼び出される場合でも、helper フィールドの状態を調べ、helper フィールドがまだ初期化されていない場合には initialize() メソッドを呼ぶことで、null ポインタ参照の可能性を回避している。getHelper() 中の isEmpty() の呼出しは同期されていないため、複数のスレッドがそれぞれ initialize() メソッドを呼び出す必要があると判断してしまうかもしれない。しかし、2つ目の Helper オブジェクトを Vector オブジェクトに追加するような競合状態は発生しない。なぜならば、synchronized 修飾された initialize() メソッドの中でも、新たな Helper オブジェクトを追加する前に helper が空かどうかをチェックしており、initialize() を実行するのは高々1つのスレッドだけであるからである。結局、最初に initialize() メソッドを実行したスレッドだけが Vector オブジェクトが空であることを観測するので、getHelper() メソッド自体は synchronized 修飾する必要はない。

適合コード (static変数として初期化)

以下の適合コードでは、helper フィールドを static 変数として初期化しているため、そのフィールドを通じて参照されるオブジェクトは初期化が完了してから可視となることが保証される。

// 不変なFooクラス
final class Foo {
  private static final Helper helper = new Helper(42);

  public static Helper getHelper() {
    return helper;
  }
}

クラスが不変であることを明示するため、helper フィールドを final 宣言すべきである。

「The Java Memory Model and Thread Specification」の§9.2.3「static final 宣言されたフィールド」には以下のように記載されている[JSR-133 2004]。

クラスの初期化に関する規則により、static フィールドの読取りを行うスレッドは、そのクラスの static 初期化子と同期される。static 初期化子は、static final 宣言されたフィールドの値をセットすることのできる唯一の場所である。したがって、JMM において static final 宣言されたフィールドに関する特別な規則は必要ではない。

適合コード (不変オブジェクト - finalフィールド、volatile参照)

JMMは、オブジェクトが可視になる前に、オブジェクトの final 宣言されたフィールドの初期化が完了することを保証している[Goetz 2006a]。フィールド nfinal 宣言することで、Helper クラスは不変となる。また、「VNA01-J. 不変オブジェクトへの共有参照の可視性を確保する」に従って helper フィールドを volatile 宣言すれば、getHelper() メソッドを呼び出す任意のスレッドから Helper オブジェクトへの参照が可視となるのは、Helper オブジェクトの初期化が完了した後になることが保証される。

class Foo {
  private volatile Helper helper;

  public Helper getHelper() {
    return helper;
  }

  public void initialize() {
    helper = new Helper(42);
  }
}

// 不変なHelperクラス
public final class Helper {
  private final int n;

  public Helper(int n) {
    this.n = n;
  }
  // ...
}

この適合コードでは、helper フィールドは volatile 宣言され、Helper クラスは不変でなければならない。helper フィールドが volatile 宣言されていなければ、「VNA01-J. 不変オブジェクトへの共有参照の可視性を確保する」に違反することになる。

Helper クラスの新規インスタンスを返す public static 宣言したファクトリメソッドを定義する方法も推奨する。そうすることで、private 宣言されたコンストラクタにおいて Helper インスタンスを作成することができる。

適合コード (スレッドセーフな可変オブジェクト、volatile参照)

Helper クラスが可変ではあるがスレッドセーフである場合、Foo クラスの helper フィールドを volatile 宣言することで、Helperクラスを安全に公開することができる。

class Foo {
  private volatile Helper helper;

  public Helper getHelper() {
    return helper;
  }

  public void initialize() {
    helper = new Helper(42);
  }
}

// 可変ではあるが、スレッドセーフなHelperクラス
public class Helper {
  private volatile int n;
  private final Object lock = new Object();

  public Helper(int n) {
    this.n = n;

  }

  public void setN(int value) {
    synchronized (lock) {
      n = value;
    }
  }
}

Helper オブジェクトはその構築後に状態が変わる可能性があるため、オブジェクトを最初に公開した後の可変メンバーの可視性を確保するための同期が必要である。この適合コードでは、フィールド n の可視性を確保するために setN() メソッドを同期している。

Helper クラスが適切に同期されない場合、Foo クラスの helper フィールドを volatile 宣言しても Helper オブジェクトの最初の公開時の可視性が保証されるだけであり、その後のクラスの状態変化が可視となる保証はない。したがって、スレッドセーフではないオブジェクトを公開する場合、その参照を volatile 宣言するだけでは不十分である。

また、Foo クラスで helper フィールドが volatile 宣言されていない場合は、フィールド n の初期化と helper フィールドへの Helper オブジェクトの書込みとの間に事前発生関係が成立するように、フィールド nvolatile 宣言する必要がある。これが必要となるのは、呼出し元(クラス Foo)が helper オブジェクトを volatile 宣言することが期待できない場合のみである。

Helper クラスは public 宣言されているので、「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」に適合するために、private 宣言されたロックを使用して同期を行っている。

例外

TSM03-J-EX0: 初期化が完了していないオブジェクトを使用させないクラスは、初期化が完了していないオブジェクトを公開してもかまわない。これを実装するには、たとえば、初期化を行うコードの最後で volatile 宣言された boolean 型の変数を true に設定し、この変数が true にセットされていなければクラスメソッドの実行を許さないようにする。

以下の適合コードにこの手法を示す。

public class Helper {
  private int n;
  private volatile boolean initialized; // 初期値はfalseである

  public Helper(int n) {
    this.n = n;
    this.initialized = true;
  }

  public void doSomething() {
    if (!initialized) {
      throw new SecurityException(
          "Cannot use partially initialized instance");
    }
    // ...
  }
  // ...
}

この手法では、Helper クラスのすべてのメソッドでインスタンスの初期化が完了しているかチェックしているため、初期化が完了する前にインスタンスへの参照が公開されても、インスタンスが使用されることはない。

リスク評価

可変の共有データへのアクセスを同期しないと、スレッドによってオブジェクトの状態が異なって見えたり、初期化が完了していないオブジェクトが参照されたりする危険がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

TSM03-J

P8

L2

自動検出
ツール バージョン チェッカー 説明
CodeSonar
4.5p1
FB.MT_CORRECTNESS.DC_PARTIALLY_CONSTRUCTED Possible exposure of partially initialized object
参考文献

[API 2006]


[Bloch 2001]

Item 48, "Synchronize Access to Shared Mutable Data"

[Goetz 2006a]

Section 3.5.3, "Safe Publication Idioms"

[Goetz 2007]

Pattern #2, "One-Time Safe Publication"

[JPL 2006]

Section 14.10.2, "Final Fields and Security"

[Pugh 2004]


翻訳元

これは以下のページを翻訳したものです。

TSM03-J. Do not publish partially initialized objects (revision 181)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter