为什么创建一个线程被称为昂贵的?

24 浏览
0 Comments

为什么创建一个线程被称为昂贵的?

Java教程说创建一个线程是很昂贵的。但是,为什么创建线程很昂贵?当Java线程被创建时,具体发生了什么使得其创建变得昂贵?我相信这种说法是真实的,但我只对JVM中线程创建机制感兴趣。

引用块:

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

来自Java并发实践

作者:Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,David Holmes,Doug Lea

打印ISBN-10:0-321-34960-1。

admin 更改状态以发布 2023年5月22日
0
0 Comments

其他人已经讨论过线程成本的来源。本答案解释了为什么创建一个线程不如许多操作那样昂贵,但与相对廉价的任务执行替代方案相比,相对昂贵。

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

另一个选择是使用线程池。线程池可以更高效的原因有两个。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 queue = new LinkedBlockingQueue();
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。在许多,如果不是大多数情况下,这可以被认为是平凡的。相对而言,它比替代方案更昂贵,对于某些情况,线程池或根本不使用线程是更好的解决方案。

0
0 Comments

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

因为它确实是昂贵的。

Java线程的创建是昂贵的,因为涉及到很多工作:

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

此外,由于线程一直存在,它会占用资源;例如线程堆栈、从堆栈可达的任何对象、JVM线程描述符以及操作系统本地线程描述符。

所有这些成本都是特定于平台的,但在我所用过的任何Java平台上,这些成本都不便宜。


通过谷歌搜索,我找到了一个旧的基准测试报告,报告了在运行2002年版Linux系统的Sun Java 1.4.1上,每秒约能创建约4000个线程。更现代的平台会得到更好的数字……我无法对方法学进行评论……但至少它为线程创建的昂贵程度提供了一个大致的估计。

Peter Lawrey的基准测试表明,现在绝对意义上的线程创建速度要快得多,但不清楚这一点是由于Java和/或操作系统的改进,还是由于更高的处理器速度。但他的数字仍然表明,如果使用线程池而不是每次创建/启动新线程,会有150倍以上的改进。(他认为这一切都是相对的……)


以上假设使用本地线程而不是绿色线程,但现代JVM出于性能原因都使用本地线程。绿色线程可能更便宜,但您付出的代价是其他方面的性能。

更新:OpenJDK Loom项目旨在提供标准Java线程的轻量级替代方案等功能。他们提出的虚拟线程是本地线程和绿色线程的混合体。简而言之,虚拟线程就像是使用本地线程在并行执行时下面的绿色线程实现。

截至目前(2021年1月),Loom项目仍处于原型阶段,没有针对发布的Java版本。(AFAIK)


我已经做了一些研究,看看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 mmapMAP_ANONYMOUS 标志会使内存初始化为零。

因此,即使新的 Java 线程堆栈不需要被清零(按照 JVM 规范),但实际上(至少在 Linux 上的 OpenJDK 6 上)它们会被清零。

0