简体   繁体   English

为什么说创建一个线程很昂贵?

[英]Why is creating a Thread said to be expensive?

The Java tutorials say that creating a Thread is expensive. Java 教程说创建一个线程是昂贵的。 But why exactly is it expensive?但究竟为什么贵呢? What exactly is happening when a Java Thread is created that makes its creation expensive?创建 Java 线程时究竟发生了什么使其创建成本高昂? I'm taking the statement as true, but I'm just interested in mechanics of Thread creation in JVM.我认为这句话是正确的,但我只是对 JVM 中的线程创建机制感兴趣。

Thread lifecycle overhead.线程生命周期开销。 Thread creation and teardown are not free.线程创建和拆卸不是免费的。 The actual overhead varies across platforms, but thread creation takes time, introducing latency into request processing, and requires some processing activity by the JVM and OS.实际开销因平台而异,但线程创建需要时间,将延迟引入请求处理,并且需要 JVM 和操作系统进行一些处理活动。 If requests are frequent and lightweight, as in most server applications, creating a new thread for each request can consume significant computing resources.如果请求频繁且轻量级,如在大多数服务器应用程序中,为每个请求创建一个新线程会消耗大量计算资源。

From Java Concurrency in PracticeJava 并发实践开始
By Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea作者:布赖恩·戈茨、蒂姆·佩尔斯、约书亚·布洛赫、约瑟夫·鲍比尔、大卫·霍姆斯、道格·利
Print ISBN-10: 0-321-34960-1打印 ISBN-10:0-321-34960-1

Why is creating a Thread said to be expensive?为什么创建一个线程昂贵?

Because it >>is<< expensive.因为它>>是<<昂贵的。

Java thread creation is expensive because there is a fair bit of work involved: Java 线程的创建成本很高,因为涉及到相当多的工作:

  • A large block of memory has to be allocated and initialized for the thread stack.必须为线程堆栈分配和初始化一大块内存。
  • System calls need to be made to create / register the native thread with the host OS.需要进行系统调用以在主机操作系统中创建/注册本机线程。
  • Descriptors need to be created, initialized and added to JVM-internal data structures.需要创建、初始化描述符并将其添加到 JVM 内部数据结构中。

It is also expensive in the sense that the thread ties down resources as long as it is alive;从某种意义上说,只要线程还活着,它就会占用资源,这也是昂贵的。 eg the thread stack, any objects reachable from the stack, the JVM thread descriptors, the OS native thread descriptors.例如线程堆栈、任何可从堆栈访问的对象、JVM 线程描述符、OS 本地线程描述符。

The costs of all of these things are platform specific, but they are not cheap on any Java platform I've ever come across.所有这些东西的成本都是特定于平台的,但在我遇到的任何 Java 平台上都不便宜。


A Google search found me an old benchmark that reports a thread creation rate of ~4000 per second on a Sun Java 1.4.1 on a 2002 vintage dual processor Xeon running 2002 vintage Linux.在 Google 搜索中,我找到了一个旧的基准测试,它报告在运行 2002 年老式 Linux 的 2002 年老式双处理器 Xeon 上的 Sun Java 1.4.1 上的线程创建速率约为每秒 4000。 A more modern platform will give better numbers ... and I can't comment on the methodology ... but at least it gives a ballpark for how expensive thread creation is likely to be.一个更现代的平台将提供更好的数字......我无法评论该方法......但至少它为线程创建可能有多昂贵提供了一个大概。

Peter Lawrey's benchmarking indicates that thread creation is significantly faster these days in absolute terms, but it is unclear how much of this is due improvements in Java and/or the OS ... or higher processor speeds. Peter Lawrey 的基准测试表明,如今线程创建的绝对速度明显更快,但尚不清楚其中有多少是由于 Java 和/或操作系统的改进……或更高的处理器速度。 But his numbers still indicate a 150+ fold improvement if you use a thread pool versus creating/starting a new thread each time.但是他的数字仍然表明,如果您使用线程池与每次创建/启动新线程相比,性能提高了 150 多倍。 (And he makes the point that this is all relative ...) (他指出这都是相对的......)


The above assumes native threads rather than green threads, but modern JVMs all use native threads for performance reasons.上面假设本地线程而不是绿色线程,但现代 JVM 出于性能原因都使用本地线程。 Green threads are possibly cheaper to create, but you pay for it in other areas.创建绿色线程可能更便宜,但您需要在其他领域付费。

Update: The OpenJDK Loom project aims to provide a light-weight alternative to standard Java threads, among other things.更新:OpenJDK Loom 项目旨在提供标准 Java 线程的轻量级替代方案等。 The are proposing virtual threads which are a hybrid of native threads and green threads.他们提出了虚拟线程,它是本机线程和绿色线程的混合体。 In simple terms, a virtual thread is rather like a green thread implementation that uses native threads underneath when parallel execution is required.简单来说,虚拟线程更像是一个绿色线程实现,它在需要并行执行时使用底层的本地线程。

As of now (Jan 2021) the Project Loom work is still at the prototyping stage, with (AFAIK) no Java version targeted for the release.截至目前(2021 年 1 月),Project Loom 工作仍处于原型设计阶段,(AFAIK)没有针对该版本的 Java 版本。


I've done a bit of digging to see how a Java thread's stack really gets allocated.我已经做了一些挖掘,看看 Java 线程的堆栈是如何真正得到分配的。 In the case of OpenJDK 6 on Linux, the thread stack is allocated by the call to pthread_create that creates the native thread.在 Linux 上的 OpenJDK 6 的情况下,线程堆栈是通过调用pthread_create来分配的,该调用创建了本机线程。 (The JVM does not pass pthread_create a preallocated stack.) (JVM 不会通过pthread_create预分配堆栈。)

Then, within pthread_create the stack is allocated by a call to mmap as follows:然后,在pthread_create ,通过调用mmap来分配堆栈,如下所示:

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

According to man mmap , the MAP_ANONYMOUS flag causes the memory to be initialized to zero.根据man mmapMAP_ANONYMOUS标志导致内存被初始化为零。

Thus, even though it might not be essential that new Java thread stacks are zeroed (per the JVM spec), in practice (at least with OpenJDK 6 on Linux) they are zeroed.因此,即使将新的 Java 线程堆栈归零(根据 JVM 规范)可能不是必需的,但实际上(至少在 Linux 上的 OpenJDK 6 中)它们被归零。

Others have discussed where the costs of threading come from.其他人已经讨论了螺纹加工成本的来源。 This answer covers why creating a thread is not that expensive compared to many operations, but relatively expensive compared to task execution alternatives, which are relatively less expensive.这个答案涵盖了为什么与许多操作相比,创建线程并不那么昂贵,但与相对便宜的任务执行替代方案相比却相对昂贵。

The most obvious alternative to running a task in another thread is to run the task in the same thread.在另一个线程中运行任务最明显的替代方法是在同一线程中运行该任务。 This is difficult to grasp for those assuming that more threads are always better.对于那些认为线程越多越好的人来说,这很难理解。 The logic is that if the overhead of adding the task to another thread is greater than the time you save, it can be faster to perform the task in the current thread.逻辑是,如果将任务添加到另一个线程的开销大于您节省的时间,则在当前线程中执行任务可以更快。

Another alternative is to use a thread pool.另一种选择是使用线程池。 A thread pool can be more efficient for two reasons.由于两个原因,线程池可以更高效。 1) it reuses threads already created. 1)它重用已经创建的线程。 2) you can tune/control the number of threads to ensure you have optimal performance. 2) 您可以调整/控制线程数以确保获得最佳性能。

The following program prints....以下程序打印....

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

This is a test for a trivial task which exposes the overhead of each threading option.这是对一项琐碎任务的测试,它暴露了每个线程选项的开销。 (This test task is the sort of task that is actually best performed in the current thread.) (这个测试任务实际上是在当前线程中执行得最好的那种任务。)

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

As you can see, creating a new thread only costs ~70 µs.如您所见,创建一个新线程只需要大约 70 微秒。 This could be considered trivial in many, if not most, use cases.在许多(如果不是大多数)用例中,这可能被认为是微不足道的。 Relatively speaking it is more expensive than the alternatives and for some situations a thread pool or not using threads at all is a better solution.相对而言,它比替代方案更昂贵,并且在某些情况下,线程池或根本不使用线程是更好的解决方案。

In theory, this depends on the JVM.理论上,这取决于JVM。 In practice, every thread has a relatively large amount of stack memory (256 KB per default, I think).实际上,每个线程都有相对大量的堆栈内存(我认为默认为 256 KB)。 Additionally, threads are implemented as OS threads, so creating them involves an OS call, ie a context switch.此外,线程被实现为操作系统线程,因此创建它们涉及操作系统调用,即上下文切换。

Do realize that "expensive" in computing is always very relative.一定要意识到计算中的“昂贵”总是非常相对的。 Thread creation is very expensive relative to the creation of most objects, but not very expensive relative to a random harddisk seek.与大多数对象的创建相比,线程创建的开销非常大,但相对于随机硬盘查找而言,开销并不大。 You don't have to avoid creating threads at all costs, but creating hundreds of them per second is not a smart move.您不必不惜一切代价避免创建线程,但每秒创建数百个线程并不是明智之举。 In most cases, if your design calls for lots of threads, you should use a limited-size thread pool.在大多数情况下,如果您的设计需要大量线程,您应该使用有限大小的线程池。

There are two kinds of threads:有两种线程:

  1. Proper threads : these are abstractions around the underlying operating system's threading facilities.适当的线程:这些是围绕底层操作系统线程设施的抽象。 Thread creation is, therefore, as expensive as the system's -- there's always an overhead.因此,线程创建与系统一样昂贵——总是有开销的。

  2. "Green" threads : created and scheduled by the JVM, these are cheaper, but no proper paralellism occurs. “绿色”线程:由 JVM 创建和调度,这些线程更便宜,但不会发生适当的并行。 These behave like threads, but are executed within the JVM thread in the OS.这些行为类似于线程,但在操作系统的 JVM 线程中执行。 They are not often used, to my knowledge.据我所知,它们并不经常使用。

The biggest factor I can think of in the thread-creation overhead, is the stack-size you have defined for your threads.在线程创建开销中我能想到的最大因素是您为线程定义的堆栈大小 Thread stack-size can be passed as a parameter when running the VM.线程堆栈大小可以在运行 VM 时作为参数传递。

Other than that, thread creation is mostly OS-dependent, and even VM-implementation-dependent.除此之外,线程创建主要依赖于操作系统,甚至依赖于虚拟机实现。

Now, let me point something out: creating threads is expensive if you're planning on firing 2000 threads per second, every second of your runtime.现在,让我指出一点:如果您计划在运行时的每一秒内每秒触发2000 个线程,那么创建线程的成本是很高的 The JVM is not designed to handle that . JVM 不是为处理这个而设计的 If you'll have a couple of stable workers that won't be fired and killed over and over, relax.如果你有几个稳定的工人不会被一遍又一遍地解雇和杀害,那就放松点。

Creating Threads requires allocating a fair amount of memory since it has to make not one, but two new stacks (one for java code, one for native code).创建Threads需要分配相当数量的内存,因为它必须创建不是一个,而是两个新堆栈(一个用于 Java 代码,一个用于本机代码)。 Use of Executors /Thread Pools can avoid the overhead, by reusing threads for multiple tasks for Executor .使用Executors /Thread Pools 可以避免开销,通过为Executor 的多个任务重用线程。

Obviously the crux of the question is what does 'expensive' mean.显然,问题的关键是“昂贵”是什么意思。

A thread needs to create a stack and initialize the stack based on the run method.一个线程需要根据run方法创建一个栈并初始化这个栈。

It needs to set up control status structures, ie, what state it's in runnable, waiting etc.它需要设置控制状态结构,即它处于可运行、等待等状态。

There's probably a good deal of synchronization around setting these things up.围绕设置这些事情可能有很多同步。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM