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


180

Java教程说创建线程很昂贵。但是为什么价格昂贵呢?当创建Java线程使创建过程变得昂贵时,究竟发生了什么?我认为该说法是正确的,但是我只是对JVM中的线程创建机制感兴趣。

线程生命周期开销。线程创建和拆除不是免费的。实际开销因平台而异,但是线程创建会花费时间,从而在请求处理中引入延迟,并且需要JVM和OS进行某些处理活动。如果请求频繁且轻量(如大多数服务器应用程序中一样),则为每个请求创建一个新线程可能会消耗大量计算资源。

来自Java并发实践
作者:Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,David Holmes,Doug Lea
打印ISBN-10:0-321-34960-1


我不知道您所阅读的教程所处的上下文是这样的:它们是暗示创建本身是昂贵的,还是“创建线程”是昂贵的。我试图显示的区别在于创建线程的纯动作(让我们称之为实例化它或其他东西),或者您拥有线程的事实(因此使用线程:显然有开销)之间。您想问哪个//您要问哪个?
Nanne 2011年

9
@typoknig-与不创建新线程相比,价格昂贵:)
willcodejavaforfood 2011年


1
线程池的胜利。无需总是为任务创建新线程。
亚历山大·米尔斯

Answers:


149

Java线程的创建非常昂贵,因为其中涉及大量工作:

  • 必须为线程堆栈分配并初始化一大块内存。
  • 需要进行系统调用以在主机OS中创建/注册本机线程。
  • 需要创建,初始化描述符并将其添加到JVM内部数据结构中。

从某种意义上说,只要线程处于活动状态,它就束缚资源,这也是昂贵的。例如线程堆栈,可从堆栈访问的任何对象,JVM线程描述符,OS本机线程描述符。

所有这些东西的成本是特定于平台的,但是在我遇到过的任何Java平台上,它们都不便宜。


谷歌搜索发现我有一个旧的基准,该基准报告在运行2002老式Linux的2002老式双处理器Xeon上,在Sun Java 1.4.1上线程创建速率约为4000每秒。一个更现代的平台将提供更多的数据……而且我无法评论该方法论……但至少,它为可能创建线程的成本提供了保证。

彼得·劳瑞(Peter Lawrey)的基准测试表明,从绝对意义上讲,如今的线程创建速度显着提高,但是目前尚不清楚其中有多少是由于Java和/或操作系统的改进或更高的处理器速度所致。但是,如果您使用线程池,而不是每次都创建/启动一个新线程,那么他的数据仍然表明可以提高150倍以上。(他指出这都是相对的...)


(以上假设“本地线程”而不是“绿色线程”,但是现代JVM出于性能原因都使用本地线程。绿色线程创建起来可能更便宜,但您可以在其他领域为此付费)。


我做了一些挖掘工作,以了解如何真正分配Java线程的堆栈。对于Linux上的OpenJDK 6,线程堆栈是通过调用分配给pthread_create创建本地线程的。(JVM不会传递pthread_create预分配的堆栈。)

然后,pthread_create通过调用将栈内分配mmap如下:

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

根据man mmap,该MAP_ANONYMOUS标志使内存初始化为零。

因此,即使并非必须(根据JVM规范)将新的Java线程栈置零,但实际上(至少对于Linux上的OpenJDK 6)将它们置零。


2
@Raedwald-初始化部分很昂贵。在某个地方,某些东西(例如GC或OS)将在将块转换为线程堆栈之前将字节清零。在典型的硬件上,这需要物理内存周期。
斯蒂芬·C

2
“在某些地方,某些东西(例如GC或OS)会将字节清零”。它会?出于安全原因,操作系统将需要分配新的内存页面。但这并不常见。操作系统可能会保留已经为零的页面的缓存(IIRC,Linux会这样做)。考虑到JVM将阻止任何Java程序读取其内容,GC为什么还要打扰?请注意,malloc()JVM可能会很好地使用标准C 函数,它不能保证分配的内存为零(大概是为了避免此类性能问题)。
Raedwald

1
stackoverflow.com/questions/2117072/…同意“一个主要因素是分配给每个线程的堆栈内存”。
拉德瓦尔德

2
@Raedwald-有关如何实际分配堆栈的信息,请参见更新的答案。
Stephen C

2
mmap()调用分配的内存页有可能(甚至可能)被写时复制映射到零页,因此它们的初始化不是发生在mmap()自身内部,而是在首次写入这些页时发生,然后在一个时间。也就是说,当线程开始执行时,创建的线程而不是创建者的线程会付出代价。
Raedwald

76

其他人则讨论了线程成本的来源。这个答案涵盖了为什么创建线程与许多操作相比并没有那么昂贵,而与任务执行替代方法相比却相对昂贵,而任务执行替代方法则相对便宜。

在另一个线程中运行任务的最明显替代方法是在同一线程中运行任务。对于那些假设更多线程总是更好的人来说,这很难理解。逻辑是,如果将任务添加到另一个线程的开销大于您节省的时间,则在当前线程中执行任务的速度会更快。

另一种选择是使用线程池。线程池可以更高效,这有两个原因。1)它重用已经创建的线程。2)您可以调整/控制线程数,以确保获得最佳性能。

以下程序打印。

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

这是对一项琐碎任务的测试,该任务暴露了每个线程选项的开销。(此测试任务实际上是在当前线程中最佳执行的任务。)

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);
    }
}
}

如您所见,创建新线程仅花费约70 µs。在很多(如果不是大多数)用例中,这可能被认为是微不足道的。相对而言,它比替代方案更昂贵,并且在某些情况下,使用线程池或根本不使用线程是更好的解决方案。


8
那是一段很棒的代码。简明扼要,清楚地显示其技巧。
尼古拉斯

在最后一个块中,我认为结果会偏斜,因为在前两个块中,主线程在工作线程放置时并行删除。但是,在最后一个块中,采取动作都是串行执行的,因此它正在扩展值。您可能可以使用queue.clear()并使用CountDownLatch来等待线程完成。
维克多·格拉齐

@VictorGrazi我假设您想集中收集结果。在每种情况下,它都进行相同数量的排队工作。倒计时闩锁会稍微快一点。
彼得·劳瑞

实际上,为什么不仅仅让它持续快速地执行某些操作,例如增加一个计数器;删除整个BlockingQueue内容。最后检查计数器,以防止编译器优化增量操作
Victor Grazi 2013年

@grazi在这种情况下可以执行此操作,但在大多数实际情况下则不会,因为在柜台上等待可能效率不高。如果您这样做的话,示例之间的差异将更大。
彼得·劳瑞

31

从理论上讲,这取决于JVM。实际上,每个线程都有相对大量的堆栈内存(我认为默认值为256 KB)。另外,线程被实现为OS线程,因此创建它们涉及OS调用,即上下文切换。

一定要意识到计算中的“昂贵”总是很亲密的。相对于大多数对象的创建,线程的创建是非常昂贵的,但是相对于随机硬盘搜索而言,线程的创建却不是很昂贵。您不必避免不惜一切代价创建线程,但是每秒创建数百个线程并不是明智之举。在大多数情况下,如果您的设计需要大量线程,则应使用有限大小的线程池。


9
Btw kb =千位,kB =千字节。Gb =千兆位,GB =千兆字节。
彼得·劳瑞

@PeterLawrey我们是否将“ k”大写为“ kb”和“ kB”,所以对“ Gb”和“ GB”具有对称性?这些东西使我烦恼。
杰克

3
@Jack有一个K= 1024和k=1000。;)en.wikipedia.org/wiki/Kibibyte
Peter Lawrey

9

有两种线程:

  1. 适当的线程:这些是围绕底层操作系统的线程设施的抽象。因此,线程创建与系统一样昂贵-总是有开销。

  2. “绿色”线程:由JVM创建和调度,这些线程比较便宜,但是不会发生适当的并行处理。它们的行为类似于线程,但是在OS的JVM线程中执行。据我所知,它们并不经常使用。

在线程创建开销中,我能想到的最大因素是为线程定义的堆栈大小。运行VM时,可以将线程堆栈大小作为参数传递。

除此之外,线程创建主要依赖于OS,甚至依赖于VM实现。

现在,让我指出一点:如果您打算在运行时的每秒每秒触发2000个线程,则创建线程的成本很高JVM并非旨在处理该问题。如果您有几个稳定的工人不会一再被解雇和杀死,请放松。


19
“……一对稳定的工人不会被解雇并杀死……”为什么我开始考虑工作场所的条件?:-)
Stephen C

6

创建Threads需要分配相当数量的内存,因为它不必创建一个,而是创建两个新堆栈(一个用于Java代码,一个用于本机代码)。通过将线程重用于Executor的多个任务,使用Executor / Thread池可以避免开销。


@Raedwald,使用单独堆栈的jvm是什么?
bestsss 2011年

1
菲利普·JP说2叠。
Raedwald

据我所知,所有JVM在每个线程中分配两个堆栈。对于垃圾收集,将Java代码(即使是JIT时)与自由广播c区别对待也很有帮助。
菲利普·JF

@Philip JF您能详细说明一下吗?2个堆栈分别用于Java代码和一个用于本机代码的含义是什么?它有什么作用?
Gurinder

“据我所知,所有JVM在每个线程中分配两个堆栈。” -我从未见过任何证据可以证明这一点。也许您误解了JVM规范中opstack的本质。(这是一种对字节码的行为进行建模的方法,不需要在运行时使用它们来执行它们。)
Stephen C

1

显然,问题的症结在于“昂贵”是什么意思。

线程需要创建堆栈并根据run方法初始化堆栈。

它需要建立控制状态结构,即它处于可运行,等待等状态。

设置这些东西可能有很多同步。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.