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
答案 0 :(得分:141)
Java线程创建很昂贵,因为涉及到相当多的工作:
从某种意义上来说它也是昂贵的,只要它存在,线程就会占用资源;例如线程堆栈,从堆栈可以访问的任何对象,JVM线程描述符,OS本机线程描述符。
所有这些东西的成本都是特定于平台的,但是在我遇到的任何Java平台上它们并不便宜。
谷歌搜索发现了一个old benchmark,它报告了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)
根据man mmap
,MAP_ANONYMOUS
标志会将内存初始化为零。
因此,尽管将新的Java线程堆栈归零(根据JVM规范)可能并不重要(实际上(至少在Linux上使用OpenJDK 6),它们都归零。
答案 1 :(得分: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
这是对一个简单任务的测试,它暴露了每个线程选项的开销。 (此测试任务是实际上在当前线程中最佳执行的任务。)
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。在许多(如果不是大多数)用例中,这可能被认为是微不足道的。相对而言,它比替代品更昂贵,并且对于某些情况,线程池或根本不使用线程是更好的解决方案。
答案 2 :(得分:29)
理论上,这取决于JVM。实际上,每个线程都有相对大量的堆栈内存(我认为默认为256 KB)。另外,线程被实现为OS线程,因此创建它们涉及OS调用,即上下文切换。
要意识到计算中的“昂贵”总是非常相对的。相对于大多数对象的创建,线程创建非常昂贵,但相对于随机硬盘搜索而言并不是非常昂贵。您不必不惜一切代价避免创建线程,但每秒创建数百个线程并不是一个聪明的举动。在大多数情况下,如果您的设计需要大量线程,则应使用有限大小的线程池。
答案 3 :(得分:8)
有两种线程:
正确的主题:这些是基础操作系统的线程设施周围的抽象。因此,线程创建与系统一样昂贵 - 总是有开销。
“绿色”线程:由JVM创建和安排,这些更便宜,但没有适当的并列发生。它们的行为类似于线程,但是在OS中的JVM线程中执行。据我所知,它们并不常用。
我可以在线程创建开销中考虑的最大因素是您为线程定义的 stack-size 。线程堆栈大小可以在运行VM时作为参数传递。
除此之外,线程创建主要依赖于操作系统,甚至依赖于VM实现。
现在,让我指出一点:如果您计划每秒触发 2000个线程,每秒运行一次,那么创建线程会很昂贵。 JVM不是为处理而设计的。如果你有几个稳定的工人不会一次又一次地被解雇和杀害,那就放松吧。
答案 4 :(得分:6)
创建Threads
需要分配相当数量的内存,因为它必须不是一个,而是两个新堆栈(一个用于java代码,一个用于本机代码)。使用Executors /线程池可以通过为Executor的多个任务重用线程来避免开销。
答案 5 :(得分:0)
显然,问题的症结在于“昂贵”意味着什么。
线程需要创建一个堆栈并根据run方法初始化堆栈。
它需要设置控制状态结构,即它在runnable,等待等状态。
关于设置这些内容可能有很多同步。