【问题标题】:Java thread creation overheadJava 线程创建开销
【发布时间】:2011-01-08 04:37:55
【问题描述】:

传统智慧告诉我们,大容量企业 Java 应用程序应优先使用线程池,而不是生成新的工作线程。使用java.util.concurrent 让这一切变得简单。

但是,确实存在不适合线程池的情况。我目前正在努力解决的具体示例是使用InheritableThreadLocal,它允许ThreadLocal 变量被“传递”到任何产生的线程。这种机制在使用线程池时会中断,因为工作线程通常不是从请求线程产生的,而是预先存在的。

现在有一些方法可以解决这个问题(可以显式传入线程局部变量),但这并不总是合适或实用的。最简单的解决方案是按需生成新的工作线程,并让InheritableThreadLocal 完成它的工作。

这让我们回到了问题 - 如果我有一个大容量站点,其中用户请求线程每个都产生六个工作线程(即不使用线程池),这会给 JVM 带来问题吗?我们可能会谈论每秒创建几百个新线程,每个线程持续时间不到一秒。现代 JVM 是否对此进行了很好的优化?我记得在 Java 中需要对象池的日子,因为创建对象的成本很高。从那以后,这变得不必要了。我想知道这是否同样适用于线程池。

如果我知道要衡量什么,我会对其进行基准测试,但我担心问题可能比使用分析器衡量的更为微妙。

注意:使用线程局部变量的智慧不是这里的问题,所以请不要建议我不要使用它们。

【问题讨论】:

  • 我打算建议将您的 ThreadLocal 包装在访问器方法中可能会解决您使用 InheritableThreadLocal 的问题,但您似乎不想听到这个。另外,您似乎将 InheritableThreadLocal 用作带外调用框架,老实说,这似乎是一种代码味道。
  • 就线程池而言,主要好处是控制:您知道您不会突然尝试在一秒钟内启动 10,000 个线程。
  • @kdgregory:对于您的第一点,有问题的 ThreadLocals 由 Spring 的 bean 作用域使用。这就是 Spring 的工作方式,而不是我可以控制的东西。对于您的第二点,入站请求线程受tomcat的线程池限制,因此限制是固有的。
  • Tomcat线程池如何限制你创建的线程数?您描述了一个应用程序,其中“用户请求线程 [spawn] 六个工作线程”,我认为您关心的是这些线程。一个错误,您可以轻松地为单个请求启动 10,000 个线程。
  • 然而,关于您需要 ThreadLocal 的原因:它是有效的,并且在消息中发布以避免聪明的 cmets 是一件好事 :-)

标签: java performance multithreading


【解决方案1】:

这是一个微基准测试示例:

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 个线程,即。 e. 线程创建开销为 0.1 毫秒。因此,在这样的机器上,每秒几百个新线程造成的开销可以忽略不计(通过比较 2 和 100 个线程的列中的数字也可以看出)。

【讨论】:

    【解决方案2】:

    首先,这当然很大程度上取决于您使用的 JVM。操作系统也将发挥重要作用。假设 Sun JVM(嗯,我们还这么称呼它吗?):

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

    这只是一个猜测,但我认为“每秒有几百个新线程”绝对超出了 JVM 旨在轻松处理的范围。我怀疑一个简单的基准测试会很快揭示一些非常微妙的问题。

    【讨论】:

    • 我发现new Thread() 的含义是一个有趣的概念。在现代 JVM 中,new Object() 并不总是分配新内存,它会重用以前垃圾收集的对象。我想知道 JVM 是否有任何原因不能有一个隐藏的、内部的可重用线程池,所以 new Thread() 不一定会创建一个新的内核线程。您将获得有效的线程池,而无需为其提供 API。
    • 如果是这样,应该可以在一些JSR中找到。可能是 133 cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf
    • @skaffman 你的假设似乎与我至少在 osx/jdk1.6 上观察到的一致。在过去的几个月里,我曾几次在线程池 +“新可运行”与类似大小的信号量 +“新线程”之间进行竞赛,但似乎从来没有任何可测量的差异。信号量方法有时似乎优于池方法,但差异是如此微小且如此罕见,以至于它实际上只是强调了它们的相似程度,您必须努力工作才能获得它们之间的任何差异。
    • 如果我总是每 3 分钟创建一个新的 Thread() 一次,....?对于在 24 小时内运行的应用程序来说,它是否仍然是昂贵的内存消耗? @Michael,解决方案是什么……?因为每 3 分钟一个线程结束,另一个线程创建.... 还会贵吗? :D
    • @gumuruh:每 3 分钟创建一个线程没有任何问题。只有当线程没有结束并且它们的堆栈内存没有被回收时,它才会成为一个问题。
    【解决方案3】:
    • 对于您的基准测试,您可以使用JMeter + 一个分析器,它可以让您直接了解在如此高负载环境中的行为。让它运行一个小时并监控内存、cpu 等。如果没有任何问题并且 cpu 没有过热,那就没关系了 :)

    • 也许您可以获得一个线程池,或者通过添加一些代码来自定义(扩展)您正在使用的线程池,以便在每次从线程池获取Thread 时设置适当的InheritableThreadLocals . 每个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() 结合使用以获得所需的行为。然而,这有点临时性,而且,我不知道它(使用反射)是否会引入比创建线程更大的开销。

    【讨论】:

    • threadlocals 的转录是我考虑过的。然而,在我的特殊情况下,我在 Spring 3 中使用@Async,它将Callable 的机制与业务逻辑分离。这很酷,但意味着您无法访问执行程序本身或创建的任务。
    • 您是否检查过 spring 是否没有一些可插入的机制来替换 executor impelementation?如果没有,那么为了进一步进行黑客攻击,您可以尝试创建一个与您最终将放置自定义代码的类具有相同限定名称的类,并让它加载而不是原始类。但这是最后的手段。
    • 嗯,是的,Spring 确实允许您指定用于@Async 的执行程序,所以是的,有一种方法可以通过那里的 threadlocals 传递,尽管正如您所说,它仍然会变得非常难看.
    【解决方案4】:

    我想知道如果每个用户请求的典型生命周期短至一秒,是否有必要在每个用户请求上生成新线程。您能否使用某种通知/等待队列来生成给定数量的(守护程序)线程,并且它们都等到有任务要解决。如果任务队列变长,您会产生额外的线程,但不是 1-1 的比率。它很可能会比产生数百个生命周期如此短的新线程更好。

    【讨论】:

    • 你描述的是一个线程池,我已经在问题中描述了。
    • 如果每个请求线程都充当线程池,我想我只是不明白为什么你不能有一个private ThreadLocal&lt;T&gt; local;,你每次请求线程唤醒时都会实例化,并且在处理每个线程时工作线程,你使用local.set()/local.get(),但很可能我误解了你的问题。
    猜你喜欢
    • 2011-04-25
    • 1970-01-01
    • 1970-01-01
    • 2019-02-10
    • 1970-01-01
    • 2016-04-12
    相关资源
    最近更新 更多