Javaの各スレッドは、生成されると同時にあるスレッドグループに割り当てられる。スレッドグループは、java.lang.ThreadGroupクラスによって実装されている。スレッドグループ名を明示的に指定しない場合、JVMがデフォルトのグループであるmainを割り当てる [Tutorials 2008]。ThreadGroup クラスのメソッドは、あるスレッドグループに属するすべてのスレッドを一度に操作するのに便利である。たとえば、ThreadGroup.interrupt()メソッドは、同一スレッドグループ内のすべてのスレッドに割り込みをかける。また、スレッドグループを使用することで、異なるグループのスレッドと干渉し合わないようにスレッドをグループ化することが可能になり、多層的にセキュリティを強化することができる[JavaThreads 2004]。
スレッドグループはスレッドを整理する上で役に立つが、ThreadGroupクラスのメソッドの多く(たとえばallowThreadSuspension()、resume()、
セキュアではないが非推奨とされていないメソッドにはたとえば以下が挙げられる。
- ThreadGroup.activeCount()
Java API仕様はactiveCount()メソッドについて以下のように記述している。[API 2006]スレッドグループ内のアクティブスレッドのおよその数を返す。
このメソッドは、スレッドの列挙(enumeration)に先立って使用されることが多い。スレッドは、たとえ開始されていない状態であっても、スレッドグループ内に存在する限り、アクティブスレッドとしてカウントされる。その上、アクティブスレッドのカウントは、ある種のシステムスレッドの存在にも影響を受ける [API 2006]。つまり、activeCount() メソッドは、スレッドグループ内で実際に実行されているタスクの正確な数を示さない可能性がある。
- ThreadGroup.enumerate()
Java API仕様はenumerate()メソッドについて以下のように記述している。[API 2006][enumerate()メソッドは] スレッドグループとそのサブグループ内の各アクティブスレッドを指定された配列にコピーする。アプリケーションは、望ましい配列のサイズを得るために activeCount() メソッドを使用すべきである。配列が小さすぎてすべてのスレッドを保持できない場合、配列に入らないスレッドは通知されることなく無視される。
ThreadGroup API を使用してスレッドを終了する場合にも落とし穴がある。stop()メソッドは非推奨であるため、スレッドを終了するには他のメソッドを用いる必要がある。『プログラミング言語Java』には以下のように記している。[JPL 2006]
一つの方法としては、他のスレッドにjoinすることで他のスレッドがいつ終了したかを知った上で、終了処理を開始する方法である。しかし、単にThreadGroupを検査するのでは、同じグループに終了しないライブラリスレッドが含まれる場合、join から戻れないため、アプリケーションは、自身が生成したスレッドのリストを保持する必要があるかもしれない。
Executorフレームワークは、スレッドの論理的なグループ分けを行うための優れたAPIを提供し、スレッドの終了処理や例外処理を安全に行うことを可能にする[Bloch 2008]。よって、プログラムから ThreadGroup のメソッドを呼び出してはならない。
違反コード
以下の違反コードは、controllerと名付けられたスレッドを保持するNetworkHandlerクラスを含んでおり、controllerスレッドは、ワーカースレッドに新しいリクエストの処理を委譲する。競合状態の例として、controllerスレッドが run()メソッドで3つのスレッドを連続して開始し、それぞれのリクエストを処理する場合を考える。すべてのスレッドは、スレッドグループ Chief に所属する。
final class HandleRequest implements Runnable { public void run() { // なんらかの処理を行う } } public final class NetworkHandler implements Runnable { private static ThreadGroup tg = new ThreadGroup("Chief"); @Override public void run() { new Thread(tg, new HandleRequest(), "thread1").start(); new Thread(tg, new HandleRequest(), "thread2").start(); new Thread(tg, new HandleRequest(), "thread3").start(); } public static void printActiveCount(int point) { System.out.println("Active Threads in Thread Group " + tg.getName() + " at point(" + point + "):" + " " + tg.activeCount()); } public static void printEnumeratedThreads(Thread[] ta, int len) { System.out.println("Enumerating all threads..."); for (int i = 0; i < len; i++) { System.out.println("Thread " + i + " = " + ta[i].getName()); } } public static void main(String[] args) throws InterruptedException { // controllerスレッドを開始する Thread thread = new Thread(tg, new NetworkHandler(), "controller"); thread.start(); // アクティブスレッド数を取得する(安全ではない) Thread[] ta = new Thread[tg.activeCount()]; printActiveCount(1); // P1 // TOCTOU条件を例示するために遅延する(競合ウィンドウ) Thread.sleep(1000); // P2: 新規スレッドを開始するのでスレッド数は変化する printActiveCount(2); // P1の時点で得たスレッド数(既に変わってしまっている)を誤って使用している int n = tg.enumerate(ta); // 新規に開始したスレッドを暗黙裡に無視している printEnumeratedThreads(ta, n); // (P1とP2の中間) // 以下のコードでは、生きているスレッドをまったく含んでいない場合は // スレッドグループを破壊する for (Thread thr : ta) { thr.interrupt(); while(thr.isAlive()); } tg.destroy(); } }
スレッド数とスレッドのリストの取得はアトミックに行われていないため、この実装には time-of-check, time-of-use (TOCTOU) 脆弱性が存在する。main()メソッドにおいて activeCount()メソッドが呼び出されてから enumerate()メソッドが呼び出されるまでの間に新規リクエストが発生した場合、グループ内のスレッド総数は増加するが、スレッドを列挙したリストである ta は、はじめに割り当てたスレッド数、つまり、mainとcontrollerの2つのスレッド参照のみを持つことになる。したがって、プログラムは、Chief スレッドグループ内で新規に実行を開始したスレッドを取り扱うことはできない。
このような配列taを使用することは安全ではない。たとえば、スレッドグループおよびそのサブグループを破壊するためにdestroy()メソッドを呼び出したとしても、期待通りには動作しないであろう。destroy()メソッド呼び出しの前提条件は、スレッドグループ内に実行中のスレッドが存在しないということである。上記コードでは、スレッドグループ内の全スレッドに割込みをかけることでこの前提条件を達成しようしている。しかし、destroy()メソッドが呼び出された時点ではスレッドグループは空ではなく、java.lang.IllegalThreadStateException がスローされる。
適合コード
以下の適合コードでは、3つのタスクをグループ化するためにThreadGroupではなく、固定長のスレッドプールを使用している。java.util.concurrent.ExecutorServiceインタフェースはスレッドプールを管理するメソッドを提供するが、アクティブなスレッド数をカウントするメソッドや、それらを列挙するメソッドは提供しない。しかし、スレッドを論理的にグループ化することで、グループ全体の振舞いを制御することが可能になる。たとえば、shutdownPool()メソッドを呼び出すことで特定のスレッドプールに属するスレッドをすべて終了させることができる。
public final class NetworkHandler { private final ExecutorService executor; NetworkHandler(int poolSize) { this.executor = Executors.newFixedThreadPool(poolSize); } public void startThreads() { for (int i = 0; i < 3; i++) { executor.execute(new HandleRequest()); } } public void shutdownPool() { executor.shutdown(); } public static void main(String[] args) { NetworkHandler nh = new NetworkHandler(3); nh.startThreads(); nh.shutdownPool(); } }
Java SE 5.0より前では、他のスレッドで未捕捉例外(uncaught exception)をキャッチしようとした場合、ThreadGroupクラスを継承するよりほかなかった。UncaughtExceptionHandlerを実装しようとした場合、ThreadGroupをサブクラス化することによってのみ実現することができたからである。最近のJavaでは、Threadクラスに定義されたインタフェースを使用することで UncaughtExceptionHandler をスレッド単位で実装できる。したがって ThreadGroupクラスでのみ実現できる機能はほとんど残っていない[Goetz 2006][Bloch 2008]。
スレッドプールにおける未捕捉例外ハンドラの使用の詳細に関しては、「TPS03-J. スレッドプールで実行されるタスクを通知なしに異常終了させない」を参照のこと。
リスク評価
ThreadGroup APIを使用すると、競合状態、メモリリーク、一貫性を欠いたオブジェクト状態などを引き起こす恐れがある。
ルール | 脅威度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
THI01-J | 低 | 中 | 中 | P4 | L3 |
参考文献
[API 2006] | Methods activeCount and enumerate; Classes ThreadGroup and Thread |
[Bloch 2001] | Item 53. Avoid thread groups |
[Bloch 2008] | Item 73. Avoid thread groups |
[Goetz 2006] | Section 7.3.1, Uncaught Exception Handlers |
[JavaThreads 04] | 13.1, ThreadGroups |
[JPL 2006] | 23.3.3, Shutdown Strategies |
[SDN 2006] | Bug ID 4089701 and 4229558 |
[Tutorials 2008] |
翻訳元
これは以下のページを翻訳したものです。
THI01-J. Do not invoke ThreadGroup methods (revision 93)