《Java并发编程实践》线程生命周期开销。线程创建和销毁并不是免费的。实际开销因平台而异,但是线程创建需要时间,会引入请求处理的延迟,并且需要JVM和操作系统进行一些处理活动。如果请求频繁且轻量级(如大多数服务器应用程序),为每个请求创建一个新线程可能会消耗大量计算资源。
作者:Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes、Doug Lea
印刷版 ISBN-10: 0-321-34960-1
《Java并发编程实践》线程生命周期开销。线程创建和销毁并不是免费的。实际开销因平台而异,但是线程创建需要时间,会引入请求处理的延迟,并且需要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
标志会导致内存被初始化为零。mmap()
调用分配的内存页很可能(甚至是可能性更大)被写时复制映射到一个零页面,因此它们的初始化不是在 mmap()
内部发生,而是当页面首次被写入时,每次只有一页被初始化。也就是说,当线程开始执行时,这个代价由创建的线程承担而非创建线程。 - Raedwald其他人已经讨论了线程成本的来源。这个答案涵盖了为什么创建一个线程不像许多操作那样昂贵,但与执行任务的替代方案相比,它是相对昂贵的。
在另一个线程中运行任务的最明显的替代方案是在同一线程中运行任务。对于那些认为越多线程总是更好的人来说,这很难理解。逻辑是如果将任务添加到另一个线程的开销大于节省的时间,则在当前线程中执行任务可能更快。
另一个选择是使用线程池。线程池可以更有效率,原因如下: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);
}
}
}
理论上,这取决于JVM。实际上,每个线程都有相对较大的堆栈内存(默认为256 KB)。此外,线程是作为操作系统线程实现的,因此创建它们涉及到一个操作系统调用,即上下文切换。
请注意,在计算机中,“昂贵”是非常相对的概念。相对于大多数对象的创建而言,线程创建非常昂贵,但相对于随机硬盘寻道来说就不是很昂贵了。您不必一定要避免创建线程,但每秒创建数百个线程并不是一个明智的选择。在大多数情况下,如果你的设计需要大量线程,你应该使用有限大小的线程池。
K
等于1024,而k
等于1000。;) 参考维基百科上的“Kibibyte”词条。 - Peter Lawrey有两种类型的线程:
真正的线程:这是对底层操作系统线程设施的抽象。因此,线程的创建费用与系统的费用一样昂贵,存在开销。
“绿色”线程:由JVM创建和调度,这些线程更便宜,但不会出现真正的并行性。它们像线程一样运行,但在OS中的JVM线程中执行。据我所知,它们并不经常被使用。
我能想到的影响线程创建开销的最大因素是您为线程定义的堆栈大小。当运行VM时,可以将线程堆栈大小作为参数传递。
除此之外,线程创建主要取决于操作系统,并且甚至取决于VM实现。
现在,让我指出一些事情:如果您计划每秒启动2000个线程,持续运行,那么创建线程是很昂贵的。JVM没有设计来处理这样的情况。如果您只需要一些稳定的工作线程,它们不会一遍又一遍地启动和关闭,请放心使用。
显然问题的关键在于“昂贵”的含义是什么。
一个线程需要创建一个堆栈并基于运行方法初始化该堆栈。
它需要设置控制状态结构,即,它所处的状态(可运行、等待等)。
在设置这些内容时可能需要进行大量的同步。