为什么创建线程被认为是昂贵的?

Java教程说创建线程是昂贵的。但究竟为什么它很贵呢?当Java线程被创建时,究竟发生了什么,使得它的创建成本高昂?我认为这句话是正确的,但我只是对JVM中线程创建的机制感兴趣。

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

From Java Concurrency in Practice
By Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea
Print ISBN-10: 0-321-34960-1


答案 1

为什么创建线程被认为是昂贵的?

因为它>><<昂贵。

Java线程的创建成本很高,因为涉及相当多的工作:

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

从某种意义上说,它也很昂贵,因为只要线程处于活动状态,它就会束缚资源;例如,线程堆栈,可以从堆栈访问的任何对象,JVM线程描述符,操作系统本机线程描述符。

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


谷歌搜索发现了一个旧的基准测试,它报告在运行2002年老式Linux的2002年老式双处理器Xeon上的Sun Java 1.4.1上的线程创建速率约为每秒4000次。一个更现代化的平台将提供更好的数字...我不能评论方法论...但至少它为线程创建可能有多昂贵提供了一个大概。

Peter Lawrey的基准测试表明,从绝对值来看,这些天线程的创建速度要快得多,但目前尚不清楚其中有多少是由于Java和/或操作系统的改进......或更高的处理器速度。但他的数字仍然表明,如果你使用线程池而不是每次创建/启动一个新线程,则改进了150倍以上。(他指出,这都是相对的...)


以上假设是本机线程而不是绿色线程,但出于性能原因,现代 JVM 都使用本机线程。创建绿色线可能更便宜,但您在其他领域需要付费。

更新:OpenJDK Loom项目旨在为标准Java线程提供轻量级的替代方案。他们提出了虚拟线程,它们是本机线程和绿色线程的混合体。简单来说,虚拟线程更像是绿色线程实现,当需要并行执行时,它使用下面的本机线程。

截至目前(2021年1月),Project Loom的工作仍处于原型设计阶段,(AFAIK)没有针对该版本的Java版本。


我做了一些挖掘,看看Java线程的堆栈是如何真正分配的。在 Linux 上的 OpenJDK 6 中,线程堆栈由创建本机线程的调用分配。(JVM 不传递预分配的堆栈。pthread_createpthread_create

然后,在堆栈中通过调用分配,如下所示:pthread_createmmap

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

根据 ,该标志导致内存初始化为零。man mmapMAP_ANONYMOUS

因此,即使新的Java线程堆栈可能不必归零(根据JVM规范),但在实践中(至少在Linux上的OpenJDK 6中),它们被清零。


答案 2

其他人已经讨论了线程的成本来自哪里。这个答案涵盖了为什么与许多操作相比,创建线程并不那么昂贵,但与任务执行替代方案相比相对昂贵,后者相对便宜。

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

另一种方法是使用线程池。线程池可能更有效,原因有两个。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。这在许多(如果不是大多数)用例中可能被认为是微不足道的。相对而言,它比替代方案更昂贵,在某些情况下,线程池或根本不使用线程是更好的解决方案。


推荐