従来の通念では、大規模なエンタープライズJavaアプリケーションは、新しいワーカースレッドを生成するよりも、スレッドプールを使用する必要があります。Java.util.concurrent
を使用すると、これが簡単になります。
ただし、スレッドプールが適切でない状況が存在します。私が現在取り組んでいる特定の例は、InheritableThreadLocal
の使用です。これにより、ThreadLocal
変数を生成されたスレッドに「渡す」ことができます。ワーカースレッドは通常、要求スレッドから生成されませんが、既存のものであるため、スレッドプールを使用すると、このメカニズムは機能しなくなります。
現在、これを回避する方法があります(スレッドローカルを明示的に渡すことができます)が、これは常に適切または実用的であるとは限りません。最も簡単な解決策は、オンデマンドで新しいワーカースレッドを生成し、InheritableThreadLocal
にその仕事をさせることです。
これで質問に戻ります-ユーザーリクエストスレッドがそれぞれ半ダースのワーカースレッドを生成している(つまり、スレッドプールを使用していない)大量のサイトがある場合、これはJVMに問題を引き起こしますか?毎秒数百の新しいスレッドが作成され、それぞれが1秒未満続くことについて話している可能性があります。最新のJVMはこれをうまく最適化しますか?オブジェクトの作成には費用がかかるため、Javaでオブジェクトプーリングが望まれていた時代を覚えています。それ以来、これは不要になりました。同じことがスレッドプールにも当てはまるのだろうか。
何を測定するかがわかっていればベンチマークを行いますが、プロファイラーで測定できるよりも問題が微妙である可能性があるのではないかと心配しています。
注:スレッドローカルを使用することの知恵はここでは問題ではないので、私がそれらを使用しないことを提案しないでください。
マイクロベンチマークの例を次に示します。
public class ThreadSpawningPerformanceTest {
static long test(final int threadCount, final int workAmountPerThread) throws InterruptedException {
Thread[] tt = new Thread[threadCount];
final int[] aa = new int[tt.length];
System.out.print("Creating "+tt.length+" Thread objects... ");
long t0 = System.nanoTime(), t00 = t0;
for (int i = 0; i < tt.length; i++) {
final int j = i;
tt[i] = new Thread() {
public void run() {
int k = j;
for (int l = 0; l < workAmountPerThread; l++) {
k += k*k+l;
}
aa[j] = k;
}
};
}
System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
System.out.print("Starting "+tt.length+" threads with "+workAmountPerThread+" steps of work per thread... ");
t0 = System.nanoTime();
for (int i = 0; i < tt.length; i++) {
tt[i].start();
}
System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
System.out.print("Joining "+tt.length+" threads... ");
t0 = System.nanoTime();
for (int i = 0; i < tt.length; i++) {
tt[i].join();
}
System.out.println(" Done in "+(System.nanoTime()-t0)*1E-6+" ms.");
long totalTime = System.nanoTime()-t00;
int checkSum = 0; //display checksum in order to give the JVM no chance to optimize out the contents of the run() method and possibly even thread creation
for (int a : aa) {
checkSum += a;
}
System.out.println("Checksum: "+checkSum);
System.out.println("Total time: "+totalTime*1E-6+" ms");
System.out.println();
return totalTime;
}
public static void main(String[] kr) throws InterruptedException {
int workAmount = 100000000;
int[] threadCount = new int[]{1, 2, 10, 100, 1000, 10000, 100000};
int trialCount = 2;
long[][] time = new long[threadCount.length][trialCount];
for (int j = 0; j < trialCount; j++) {
for (int i = 0; i < threadCount.length; i++) {
time[i][j] = test(threadCount[i], workAmount/threadCount[i]);
}
}
System.out.print("Number of threads ");
for (long t : threadCount) {
System.out.print("\t"+t);
}
System.out.println();
for (int j = 0; j < trialCount; j++) {
System.out.print((j+1)+". trial time (ms)");
for (int i = 0; i < threadCount.length; i++) {
System.out.print("\t"+Math.round(time[i][j]*1E-6));
}
System.out.println();
}
}
}
Intel Core2 Duo E6400 @ 2.13GHzでの32ビットSunのJava 1.6.0_21クライアントVM)を使用した64ビットWindows7での結果は次のとおりです。
Number of threads 1 2 10 100 1000 10000 100000
1. trial time (ms) 346 181 179 191 286 1229 11308
2. trial time (ms) 346 181 187 189 281 1224 10651
結論:私のコンピューターには2つのコアがあるため、2つのスレッドは1つのスレッドのほぼ2倍の速度で動作します。私のコンピューターは毎秒約10000スレッドを生成できます。 e。 スレッド作成のオーバーヘッドは0.1ミリ秒です。したがって、このようなマシンでは、1秒あたり数百の新しいスレッドが発生するオーバーヘッドはごくわずかです(2スレッドと100スレッドの列の数を比較することでもわかります)。
まず第一に、これはもちろん、使用するJVMに大きく依存します。 OSも重要な役割を果たします。 Sun JVMを想定すると(うーん、それでもそれと呼んでいますか?):
主な要因の1つは、各スレッドに割り当てられたスタックメモリです。これは、-Xssn
JVMパラメータを使用して調整できます。回避できる最小値を使用する必要があります。
これは単なる推測ですが、「毎秒数百の新しいスレッド」は、JVMが快適に処理できるように設計されているものを確実に超えていると思います。単純なベンチマークでは、非常に微妙でない問題がすぐに明らかになると思います。
ベンチマークには JMeter +プロファイラーを使用できます。これにより、このような高負荷環境での動作の概要を直接確認できます。 1時間実行して、メモリやCPUなどを監視します。何も壊れず、CPUが過熱しない場合は、問題ありません:)
おそらく、スレッドプールを取得するか、コードを追加して使用しているスレッドプールをカスタマイズ(拡張)して、スレッドからInheritableThreadLocal
を取得するたびに適切なThread
sを設定することができます。 -プール。各Thread
には、次のパッケージプライベートプロパティがあります。
_/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
_
これらを(リフレクションを使用して)Thread.currentThread()
と組み合わせて使用すると、目的の動作を実現できます。ただし、これは少しアドホックであり、さらに、(リフレクションを使用して)スレッドを作成するよりもさらに大きなオーバーヘッドが発生しないかどうかはわかりません。
通常のライフサイクルが1秒と短い場合、ユーザーの要求ごとに新しいスレッドを生成する必要があるかどうか疑問に思っています。ある種の通知/待機キューを使用して、指定された数の(デーモン)スレッドを生成し、それらはすべて、解決するタスクがあるまで待機します。タスクキューが長くなると、追加のスレッドが生成されますが、1対1の比率では生成されません。ライフサイクルが非常に短い何百もの新しいスレッドを生成するよりも、パフォーマンスが向上する可能性があります。