Java线程创build开销

传统观点告诉我们,大批量的企业Java应用程序应该优先使用线程池来产生新的工作线程。 java.util.concurrent的使用使得这个直接。

但是,在某些情况下,线程池并不适合。 我目前正在摔跤的具体例子是使用InheritableThreadLocal ,它允许ThreadLocalvariables被“传递”到任何派生的线程。 这个机制在使用线程池的时候会中断,因为工作线程通常不是从请求线程派生的,而是预先存在的。

现在有办法解决(线程本地可以显式传入),但这并不总是合适的或实际的。 最简单的解决scheme是按需生成新的工作线程,并让InheritableThreadLocal完成工作。

这使我们回到这个问题 – 如果我有一个大量的网站,用户请求线程每个产生六个工作线程(即不使用线程池),这是否会给JVM一个问题? 我们可能在谈论每秒创build几百个新线程,每个线程持续不到一秒钟。 现代JVM是否优化了这个呢? 我记得在Java中需要对象池的时候,因为对象创build是昂贵的。 这从此变得不必要了。 我想知道如果同样适用于线程池。

如果我知道要衡量什么,我会以此为基准,但是我担心的是这些问题可能比用分析器可以测量的更微妙。

注意:使用线程本地人的智慧不是这里的问题,所以请不要暗示我不使用它们。

这里是一个示例microbenchmark:

 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.13 GHz上32位Sun Java 1.6.0_21客户端VM的64位Windows 7上的结果如下:

 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 

结论:由于我的电脑有两个内核,因此两个线程的工作速度几乎是预期的两倍。 我的电脑每秒可以产生近10000个线程,即线程创build开销为0.1毫秒 。 因此,在这样的机器上,每秒有几百个新线程会带来可忽略的开销(如通过比较2和100线程的列数所看到的那样)。

首先,这当然取决于你使用的JVM。 操作系统也将发挥重要作用。 假设Sun的JVM(嗯,我们仍然这样称呼它?):

其中一个主要因素是分配给每个线程的堆栈内存,您可以使用-Xssn JVM参数进行调-Xssn – 您将需要使用可以避开的最低值。

这只是一个猜测,但我认为“每秒钟有几百个新线程”绝对超出了JVMdevise的舒适处理范围。 我怀疑一个简单的基准会很快显示出相当不易的问题。

  • 对于你的基准testing,你可以使用JMeter +一个分析器,它可以让你直接了解这样一个负载很重的环境下的行为。 只要让它运行一个小时,并监视内存,CPU等。如果没有任何中断,CPU(S)不过热,这是OK的:)

  • 也许你可以得到一个线程池,或者通过添加一些代码来定制(扩展)你正在使用的线程池,以便每次从线程池获取一个Thread时,都要设置相应的InheritableThreadLocal 。 每个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; 

    你可以使用这些(好,reflection)与Thread.currentThread()相结合,以达到所需的行为。 然而,这是一个有点广告,而且我也不知道它是否会带来比创build线程更大的开销。

我想知道是否有必要为每个用户请求产生新的线程,如果他们典型的生命周期是短到一秒。 你可以使用某种types的通知/等待队列来生成给定数量的(守护进程)线程,并且他们都等待,直到有任务要解决。 如果任务队列变长,则产生额外的线程,但不会以1-1的比例。 如果生成数百个生命周期非常短的新线程,性能将会更好。