为什么创build一个线程说是昂贵的?
Java教程说创build一个Thread是很昂贵的。 但是,为什么它是昂贵的? 当创build一个Java线程时,到底发生了什么事情呢? 我将这个陈述视为真实,但是我只是对JVM中创build线程的机制感兴趣。
线程生命周期开销。 线程创build和拆卸不是免费的。 实际开销因平台而异,但线程创build需要时间,在请求处理中引入延迟,并且需要JVM和OS进行一些处理活动。 如果请求是频繁和轻量级的,就像在大多数服务器应用程序中一样,为每个请求创build一个新的线程会消耗大量的计算资源
从Java的并发实践
Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,David Holmes,Doug Lea
打印ISBN-10:0-321-34960-1
Java线程的创build是非常昂贵的,因为涉及到相当多的工作:
- 一个很大的内存块必须被分配并初始化为线程堆栈。
- 系统调用需要创build/注册本机线程与主机操作系统。
- 描述符需要被创build,初始化并被添加到JVM的内部数据结构中。
只要它活着,线程就会束缚资源,这也是很昂贵的。 例如线程堆栈,从堆栈可访问的任何对象,JVM线程描述符,OS本地线程描述符。
所有这些东西都是特定于平台的,但是在我遇到过的任何Java平台上它们并不便宜。
谷歌search发现我是一个旧的基准testing报告,在2002年的老式双处理器至强处理器上运行2002年的老式Linux,Sun Java 1.4.1上的线程创build速率为每秒4000个。 一个更现代化的平台会给出更好的数字……我不能评论这个方法……但是至less它可以给出线程创build可能会花费多less钱的问题。
Peter Lawrey的基准testing表明,如今的线程创build速度绝对速度明显加快,但是Java和/或操作系统方面的改进还不清楚,还有更快的处理器速度。 但是如果你使用线程池而不是每次创build/启动一个新的线程,他的数字仍然表示150倍的提升。 (他指出,这是相对的…)
(以上假设是“本地线程”而不是“绿色线程”,但是现代JVM都使用本地线程来实现性能。绿色线程创build起来可能更便宜,但是您在其他方面付出了代价)。
我已经做了一些挖掘,看看Java线程的堆栈是如何分配的。 对于Linux上的OpenJDK 6,线程堆栈由调用pthread_create
来分配,该线程创build本地线程。 (JVM不会传递pthread_create
预分配的堆栈。)
然后,在pthread_create
,通过调用mmap
来分配堆栈,如下所示:
mmap(0, attr.__stacksize, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
根据man mmap
, MAP_ANONYMOUS
标志使内存初始化为零。
因此,即使新的Java线程堆栈归零(根据JVM规范)可能不是必须的,但实际上(至less在Linux上的OpenJDK 6),它们都被清零。
其他人已经讨论了线程的成本来自哪里。 这个答案涵盖了与许多操作相比,创build线程并不昂贵的原因,但是与任务执行替代方法( 相对较便宜)相比,它相对昂贵。
在另一个线程中运行任务的最明显的替代方法是在同一个线程中运行任务。 对于那些假设更multithreading总是更好的人来说,这是很难理解的。 逻辑是,如果将任务添加到另一个线程的开销大于您保存的时间,则可以在当前线程中执行任务更快。
另一种select是使用线程池。 线程池可以更有效率有两个原因。 1)它重用已经创build的线程。 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
这是一个简单的任务的testing,它暴露了每个线程选项的开销。 (这个testing任务是在当前线程中最好执行的那种任务。)
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); } } }
正如你所看到的,创build一个新的线程只需要约70μs。 这在许多情况下可以被认为是微不足道的,如果不是绝大多数的话。 相对而言,它比替代品更昂贵,在某些情况下,线程池或者根本不使用线程是更好的解决scheme。
理论上这取决于JVM。 在实践中,每个线程都有相对较大的堆栈内存(我认为每个默认值为256 KB)。 此外,线程被实现为操作系统线程,因此创build它们涉及OS调用,即上下文切换。
意识到计算中的“昂贵”总是非常相对的。 创build线程相对于创build大多数对象来说是非常昂贵的,但相对于随机硬盘寻道来说,并不是非常昂贵的。 您不必不惜一切代价避免创build线程,但每秒创build数百个并不是一个明智之举。 在大多数情况下,如果您的devise需要大量线程,则应该使用有限大小的线程池。
有两种线程:
-
正确的线程 :这些是底层操作系统线程设施的抽象。 因此,创build线程和系统一样昂贵 – 总会有开销。
-
“绿色”线程 :由JVM创build和计划,这些更便宜,但不会发生适当的并列。 这些行为与线程类似,但在OS中的JVM线程内执行。 就我所知,它们并不常用。
在线程创build开销中,我能想到的最大的因素是您为线程定义的堆栈大小 。 运行虚拟机时,线程堆栈大小可以作为parameter passing。
除此之外,线程创build主要依赖于操作系统,甚至依赖于虚拟机实现。
现在,让我指出一点:如果您计划每秒钟运行2000个线程,那么每创build一次线程都是昂贵的。 JVM不是为了处理这个问题而devise的 。 如果你有几个稳定的工作人员,不会被解雇和杀害一遍又一遍,放松。
创buildThreads
需要分配相当数量的内存,因为它不得不使用一个,而是两个新的堆栈(一个用于java代码,一个用于本机代码)。 使用执行程序 /线程池可以避免开销,通过重用线程执行多个任务。
显然问题的症结在于“昂贵”的含义。
线程需要创build一个堆栈,并根据run方法初始化堆栈。
它需要build立控制状态结构,即处于可运行状态,等待状态等。
在设置这些东西时可能会有很多的同步。