java 为什么说创建一个线程是昂贵的?

rqmkfv5c  于 2023-08-02  发布在  Java
关注(0)|答案(6)|浏览(152)

Java教程说创建一个线程是昂贵的。但它到底为什么贵呢?当创建Java线程时,究竟发生了什么使其创建成本高昂?我认为这种说法是正确的,但我只是对JVM中线程创建的机制感兴趣。
线程生命周期开销。线程的创建和拆除不是免费的。实际开销因平台而异,但线程创建需要时间,这会给请求处理带来延迟,并且需要JVM和操作系统进行一些处理活动。如果请求是频繁且轻量级的,就像在大多数服务器应用程序中一样,为每个请求创建新线程可能会消耗大量的计算资源。
Java Concurrency in Practice(Java并发性实践)
作者:Brian Goetz,Tim Peierls,约书亚Bloch,Joseph Bowbeer,大卫Holmes,Doug莱亚
印刷ISBN-10:0-321-34960-1

o3imoua4

o3imoua41#

为什么创建一个线程 * 据说是 * 昂贵的?
因为“贵”。
Java线程创建是昂贵的,因为涉及相当多的工作:

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

它的代价也很高,因为只要线程是活动的,它就会占用资源;例如线程堆栈、从堆栈可到达的任何对象、JVM线程描述符、OS本机线程描述符。
所有这些东西的成本都因平台而异,但在我遇到的任何Java平台上都不便宜。
在Google搜索中,我发现了一个old benchmark,它报告在运行2002年老式Linux的2002年老式双处理器Xeon上的Sun Java 1.4.1上的线程创建速率为每秒约4000个。一个更现代化的平台会给予更好的数字。。我不能对方法论发表评论但至少它给出了一个线程创建的大概成本。
Peter Lawrey的基准测试表明,从绝对意义上讲,线程创建现在明显更快了,但目前还不清楚这其中有多少是由于Java和/或操作系统的改进。或更高的处理器速度。但是他的数字 * 仍然 * 表明,如果使用线程池,与每次创建/启动一个新线程相比,性能提高了150倍以上。(他指出,这一切都是相对的...)
上面的假设是本机线程而不是绿色线程,但是现代JVM出于性能原因都使用本机线程。绿色线程可能更便宜的创建,但你支付它在其他领域。
更新:OpenJDK Loom project旨在提供标准Java线程的轻量级替代方案。他们提出了 * 虚拟线程 *,它是本地线程和绿色线程的混合体。简单地说,虚拟线程很像绿色线程实现,当需要并行执行时,它在底层使用本机线程。
截至目前(2023年7月),Project Loom已成为JEP 444。它自Java 19以来一直处于预览状态,并建议在Java 21中发布完整版本。
我已经做了一些深入的研究,以了解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中)它们会被清零。

cgvd09ve

cgvd09ve2#

其他人已经讨论了线程的成本来自哪里。这个答案涵盖了为什么创建一个线程与许多操作相比并不昂贵,但是与任务执行替代方案相比 * 相对 * 昂贵,而任务执行替代方案 * 相对 * 便宜。
在另一个线程中运行任务的最明显的替代方法是在同一个线程中运行该任务。对于那些认为线程越多越好的人来说,这是很难理解的。其逻辑是,如果将任务添加到另一个线程的开销大于您保存的时间,则在当前线程中执行该任务可能会更快。
另一种方法是使用线程池。线程池可以更高效,原因有两个。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。这在许多(如果不是大多数)用例中可以被认为是微不足道的。相对而言,它比其他替代方案更昂贵,对于某些情况,线程池或根本不使用线程是更好的解决方案。

neekobn8

neekobn83#

理论上,这取决于JVM。实际上,每个线程都有相对较大的堆栈内存量(我认为每个默认值为256KB)。此外,线程被实现为OS线程,因此创建它们涉及OS调用,即上下文切换。
一定要意识到,计算中的“昂贵”总是非常相对的。线程创建相对于大多数对象的创建来说开销很大,但相对于随机硬盘寻道来说开销并不很大。您不必不惜一切代价避免创建线程,但每秒创建数百个线程并不是明智之举。在大多数情况下,如果您的设计需要大量线程,则应该使用大小有限的线程池。

elcex8rz

elcex8rz4#

有两种线程:
1.正确线程:这些是围绕底层操作系统的线程设施的抽象。因此,线程创建的开销与系统的开销一样大--总有开销。
1.绿色线程:由JVM创建和调度,这些更便宜,但没有适当的并行发生。它们的行为类似于线程,但在操作系统中的JVM线程内执行。据我所知,它们并不经常使用。
我能想到的线程创建开销中最大的因素是为线程定义的stack-size。线程堆栈大小可以在运行VM时作为参数传递。
除此之外,线程创建主要依赖于操作系统,甚至依赖于虚拟机实现。
现在,让我指出一点:如果你计划在运行时的每一秒都要触发**2000个线程,那么创建线程的代价是昂贵的。JVM并不是设计来处理这个问题的。如果你有几个稳定的工人,不会被解雇和杀害一遍又一遍,放松。

hfwmuf9z

hfwmuf9z5#

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

wgxvkvu9

wgxvkvu96#

很明显,问题的关键是“昂贵”是什么意思。
一个线程需要创建一个堆栈并根据run方法初始化堆栈。
它需要设置控制状态结构,即,它处于可运行状态,等待状态等。
在设置这些东西的过程中,可能会有大量的同步。

相关问题