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

kac*_*nov 173 java concurrency performance multithreading

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

线程生命周期开销.线程创建和拆解不是免费的.实际开销因平台而异,但线程创建需要时间,将延迟引入请求处理,并且需要JVM和OS的一些处理活动.如果请求频繁且轻量级,就像在大多数服务器应用程序中一样,为每个请求创建新线程会占用大量计算资源.

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

Ste*_*n C 145

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

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

从某种意义上来说它也是昂贵的,因为只要它存在,线程就会占用资源; 例如,线程堆栈,可从堆栈到达的任何对象,JVM线程描述符,OS本机线程描述符.

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


谷歌搜索发现了一个旧的基准测试,它在2002 年老式双处理器Xeon运行2002年老式Linux上的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)
Run Code Online (Sandbox Code Playgroud)

根据man mmap,该MAP_ANONYMOUS标志使存储器初始化为零.

因此,即使将新的Java线程堆栈归零(根据JVM规范)可能并不重要(实际上(至少在Linux上使用OpenJDK 6),它们也归零.

  • @Raedwald - 它是初始化部分,价格昂贵.在某些地方,某些东西(例如GC或OS)会在块变为线程堆栈之前将字节归零.这需要在典型硬件上进行物理内存循环. (2认同)
  • "在某个地方,某些东西(例如GC或OS)会将字节归零".它会?出于安全原因,操作系统将需要分配新的内存页面.但这种情况并不常见.操作系统可能会保留已经归零的页面缓存(IIRC,Linux会这样做).考虑到JVM会阻止任何Java程序读取其内容,为什么GC会烦恼呢?请注意,JVM可能使用的标准C`malloc()函数*不能*保证已分配的内存为零(可能是为了避免这种性能问题). (2认同)
  • @Raedwald - 有关如何实际分配堆栈的信息,请参阅更新的答案. (2认同)
  • 有可能(甚至可能)由`mmap()`调用分配的内存页是写时复制映射到零页,因此它们的初始化不在`mmap()`本身内,而是在页面是首先*写*到,然后一次只写一页.也就是说,当线程开始执行时,创建的线程而不是创建者线程的成本bourne. (2认同)

Pet*_*rey 71

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

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

另一种方法是使用线程池.由于两个原因,线程池可以更高效.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
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
}
Run Code Online (Sandbox Code Playgroud)

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

  • 那是一段很棒的代码.简明扼要,清晰地展示其精神. (7认同)

Mic*_*rdt 30

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

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

  • Btw kb =千位,kB =千字节.Gb =千兆位,GB =千兆字节. (8认同)
  • @Jack有一个`K` = 1024和`k` = 1000.;)https://en.wikipedia.org/wiki/Kibibyte (3认同)

sle*_*ica 8

有两种线程:

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

  2. "绿色"线程:由JVM创建和调度,这些线程更便宜,但没有发生适当的并列.它们的行为类似于线程,但是在OS中的JVM线程中执行.据我所知,它们并不经常使用.

我可以在线程创建开销中考虑的最大因素是您为线程定义的堆栈大小.线程堆栈大小可以在运行VM时作为参数传递.

除此之外,线程创建主要依赖于操作系统,甚至依赖于VM实现.

现在,让我指出一点:如果你计划每秒触发2000个线程,每秒运行一次,那么创建线程是很昂贵的.JVM不是为处理它而设计的.如果你有几个稳定的工人不会一次又一次地被解雇和杀害,请放松一下.

  • *"......一些不会被解雇和杀害的稳定工人......"*为什么我开始考虑工作条件?:-) (18认同)

Phi*_* JF 6

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