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

问题

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

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

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


#1 热门回答(128 赞)

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

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

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

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

谷歌搜索发现我aold benchmark报告了2002年老式双处理器Xeon运行2002年老式Linux的Sun Java 1.4.1上的线程创建率约为每秒4000。一个更现代化的平台将提供更好的数字...而且我无法对方法论发表评论......但至少它给出了昂贵的线程创建可能性。

Peter Lawrey的基准测试表明,从绝对意义上来说,创建线程的速度要快得多,但目前还不清楚这有多少是由于Java和/或操作系统的改进......或更快的处理器速度。但是,如果你使用线程池而不是每次都创建/启动一个新线程,那么他的数字表明改进了150倍。 (并且他指出这是相对的......)

(上面假设"本机线程"而不是"绿色线程",但现代JVM都出于性能原因使用本机线程。绿色线程可能更便宜创建,但你在其他方面付费。)

我已经做了一些挖掘,看看如何真正分配Java线程的堆栈。对于Linux上的OpenJDK 6,线程堆栈由调用topthread_create分配,这将创建本机线程。 (JVM没有通过pthread_createa预先分配的堆栈。)

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

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

根据man mmapMAP_ANONYMOUSflag使存储器初始化为零。

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


#2 热门回答(65 赞)

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

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

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


#3 热门回答(28 赞)

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

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