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

TSM02-J. クラスの初期化中にバックグラウンドスレッドを使用しない

クラスの初期化中にバックグラウンドスレッドを開始して使用する場合、クラスの初期化が循環し、デッドロック状態になるおそれがある。 たとえば、クラスの初期化を実行するメインスレッドがバックグラウンドスレッドの終了を待ち、バックグラウンドスレッドの方は、メインスレッドがクラスの初期化を終了するのを待つ状況である。このような問題は、たとえば、クラスの初期化中にバックグラウンドスレッドでデータベース接続を確立するような場合に発生しうる [Bloch 2005b]。プログラムでは、スレッドを開始する前にクラスの初期化を確実に完了しておかなければならない。

違反コード (バックグラウンドスレッド)

以下の違反コード例では、static 初期化子(クラスのロード時に1回だけ実行される static 宣言されたコードブロック)が、クラス初期化の一環としてバックグラウンドスレッドを開始している。バックグラウンドスレッドは、データベース接続を初期化しようと試みるが、dbConnection を含む ConnectionFactory クラスの全メンバが初期化されるまで待たなければならない。

public final class ConnectionFactory {
  private static Connection dbConnection;
  // 他のフィールドの定義 ...

  static {
    Thread dbInitializerThread = new Thread(new Runnable() {
        @Override public void run() {
          // データベース接続を初期化する
          try {
            dbConnection = DriverManager.getConnection("connection string");
          } catch (SQLException e) {
            dbConnection = null;
          }
        }
    });

    // 他の初期化処理, たとえば他のスレッドを開始するなど

    dbInitializerThread.start();
    try {
      dbInitializerThread.join();
    } catch (InterruptedException ie) {
      throw new AssertionError(ie);
    }
  }

  public static Connection getConnection() {
    if (dbConnection == null) {
      throw new IllegalStateException("Error initializing connection");
    }
    return dbConnection;
  }

  public static void main(String[] args) {
    // ...
    Connection connection = getConnection();
  }
}

静的に初期化されるフィールドは、それらが他のスレッドに可視となる前に、構築を完了することになっている(詳細は「TSM03-J. 初期化が完了していないオブジェクトを公開しない」を参照)。したがって、バックグラウンドスレッドは、処理を進める前に、メインスレッドの初期化が終了するのを待つ必要がある。しかし、ConnectionFactory クラスのメインスレッドは join() メソッドを呼び出して、バックグラウンドスレッドが終了するのを待っている。この相互依存関係によって、クラスの初期化処理に循環が発生し、デッドロックにつながる[Bloch 2005b]。

同様に、コンストラクタからスレッドを開始することも不適切である(詳細は「TSM01-J. オブジェクトの構築時にthis参照を逸出させない」を参照)。タスクを周期的に実行するタイマーを作成し、その開始を初期化のためのコード内で行うと、活性(liveness)の問題を誘発しうる。

適合コード (static 初期化子でバックグラウンドスレッドを使用しない)

以下の適合コードでは、static 初期化子からバックグラウンドスレッドをまったく生成せずに、すべてのフィールドをメインスレッド内で初期化している。

public final class ConnectionFactory {
  private static Connection dbConnection;
  // 他のフィールドの定義 ...

  static {
    // データベース接続を初期化する
    try {
      dbConnection = DriverManager.getConnection("connection string");
    } catch (SQLException e) {
      dbConnection = null;
    }
    // 他の初期化処理 (いかなるスレッドもまだ開始しない)
  }

  // ...
}

適合コード (ThreadLocal オブジェクト)

以下の適合コードでは、ThreadLocal オブジェクトからデータベース接続を初期化しており、各スレッドがそれぞれ固有のデータベース接続インスタンスを獲得できる。

public final class ConnectionFactory {
  private static final ThreadLocal<Connection> connectionHolder
                       = new ThreadLocal<Connection>() {
   @Override public Connection initialValue() {
     try {
       Connection dbConnection =
           DriverManager.getConnection("connection string");
       return dbConnection;
     } catch (SQLException e) {
       return null;
     }
   }
 };

  // 他のフィールドの定義...

  static {
    // 他の初期化処理 (いかなるスレッドもまだ開始しない)
  }

  public static Connection getConnection() {
    Connection connection = connectionHolder.get();
    if (connection == null) {
      throw new IllegalStateException("Error initializing connection");
    }
    return connection;
  }

  public static void main(String[] args) {
    // ...
    Connection connection = getConnection();
  }
}

static 初期化子は、オブジェクト間で共有されるクラスフィールドを初期化するために使うことができる。あるいは、initialValue() メソッドから初期化することも可能である。

例外

TSM02-J-EX0: どのフィールドにもアクセスしないスレッドであれば、クラス初期化中にバックグラウンドスレッドとして開始してもかまわない。たとえば、以下に示す ObjectPreserver クラス([Grand 2002]に基づく)は、オブジェクトが参照されなくなってもガベージコレクションの対象にならないようにするため、オブジェクト参照を格納する仕組みを提供している。

public final class ObjectPreserver implements Runnable {
  private static final ObjectPreserver lifeLine = new ObjectPreserver();

  private ObjectPreserver() {
    Thread thread = new Thread(this);
    thread.setDaemon(true);
    thread.start(); // このオブジェクトを生かし続ける
  }

  // このクラスも HashMap クラスもガベージコレクションされることはない。
  // さらに HashMap から他のオブジェクトへの参照も
  // ガベージコレクションを防ぐ
  private static final ConcurrentHashMap<Integer,Object> protectedMap
      = new ConcurrentHashMap<Integer,Object>();

  public synchronized void run() {
    try {
      wait();
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt(); // 割込みステータスのリセットを行う
    }
  }

  // このメソッドに渡されるオブジェクトは、この後
  // unpreserveObject() メソッドが呼ばれるまで保存されることになる
  public static void preserveObject(Object obj) {
    protectedMap.put(0, obj);
  }

  // 毎回同じインスタンスを返す
  public static Object getObject() {
    return protectedMap.get(0);
  }

  // オブジェクトへの保護を解除し、ガベージコレクションを可能にする
  public static void unpreserveObject() {
    protectedMap.remove(0);
  }
}

これはシングルトンクラスである(シングルトンクラスをディフェンシブにコーディングする方法について詳しくは「MSC07-J. シングルトンオブジェクトのインスタンスを複数作らない」を参照)。初期化処理には、このクラスのインスタンスを使用するバックグラウンドスレッドの作成が含まれている。スレッドは Object.wait() を呼び出すことにより無期限に待機状態となり、JVMが生存する間ずっと存在し続ける。このオブジェクトはデーモンスレッドにより管理されているので、JVMの通常の終了を妨害することはない。

初期化処理はバックグラウンドスレッドの開始を含んではいるが、開始されたバックグラウンドスレッドはフィールドへのアクセスを行うことも、活性や安全性の問題を引き起こすこともない。したがって上記のコードは、安全かつ有用な例外である。

リスク評価

クラス初期化中にバックグラウンドスレッドを開始すると、デッドロック状態を引き起こす危険がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

TSM02-J

P2

L3

自動検出
ツール バージョン チェッカー 説明
CodeSonar 4.5p1 FB.MT_CORRECTNESS.SC_START_IN_CTOR コンストラクタが Thread.start() を呼び出している
Parasoft Jtest 10.3 TRS.CSTART 実装済み
SonarQube Java Plugin 4.11 S2693  
参考文献

[Bloch 2005b]

Chapter 8, "Lazy Initialization"

[Grand 2002]

Chapter 5, "Creational Patterns, Singleton"

翻訳元

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

TSM02-J. Do not use background threads during class initialization (revision 156)

Top へ

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