Executors newCachedThreadPool() vs newFixedThreadPool() – 执行器newCachedThreadPool() vs newFixedThreadPool()

最后修改: 2020年 2月 22日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

When it comes to thread pool implementations, the Java standard library provides plenty of options to choose from. The fixed and cached thread pools are pretty ubiquitous among those implementations.

在谈到线程池实现时,Java标准库提供了大量可供选择的方案。在这些实现中,固定和缓存线程池是相当普遍的。

In this tutorial, we’re going to see how thread pools are working under the hood and then compare these implementations and their use-cases.

在本教程中,我们将看到线程池是如何在引擎盖下工作的,然后比较这些实现及其使用情况。

2. Cached Thread Pool

2.缓存线程池

Let’s take a look at how Java creates a cached thread pool when we call Executors.newCachedThreadPool():

让我们看看当我们调用Executors.newCachedThreadPool()时,Java如何创建一个缓存线程池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, 
      new SynchronousQueue<Runnable>());
}

Cached thread pools are using “synchronous handoff” to queue new tasks. The basic idea of synchronous handoff is simple and yet counter-intuitive: One can queue an item if and only if another thread takes that item at the same time. In other words, the SynchronousQueue can not hold any tasks whatsoever.

缓存线程池正在使用 “同步交接 “来排查新任务。同步交接的基本思想很简单,但也是反直觉的。当且仅当另一个线程同时采取该项目时,人们可以排队。换句话说,SynchronousQueue不能容纳任何任务。

Suppose a new task comes in. If there is an idle thread waiting on the queue, then the task producer hands off the task to that thread. Otherwise, since the queue is always full, the executor creates a new thread to handle that task.

假设有一个新的任务进来。如果队列中有一个空闲的线程在等待,那么任务生产者就会把任务交给该线程。否则,由于队列总是满的,执行者会创建一个新的线程来处理这个任务

The cached pool starts with zero threads and can potentially grow to have Integer.MAX_VALUE threads. Practically, the only limitation for a cached thread pool is the available system resources.

缓存的线程池从零开始,有可能增长到Integer.MAX_VALUE线程。实际上,缓存线程池的唯一限制是可用的系统资源。

To better manage system resources, cached thread pools will remove threads that remain idle for one minute.

为了更好地管理系统资源,缓存的线程池将删除保持闲置一分钟的线程。

2.1. Use Cases

2.1.使用案例

The cached thread pool configuration caches the threads (hence the name) for a short amount of time to reuse them for other tasks. As a result, it works best when we’re dealing with a reasonable number of short-lived tasks. 

缓存线程池配置将线程(因此而得名)缓存了很短的时间,以便在其他任务中重用它们。因此,当我们处理合理数量的短命任务时,它的效果最好。

The key here is “reasonable” and “short-lived”. To clarify this point, let’s evaluate a scenario where cached pools aren’t a good fit. Here we’re going to submit one million tasks each taking 100 micro-seconds to finish:

这里的关键是 “合理 “和 “短命”。为了澄清这一点,让我们评估一个缓存池不适合的场景。在这里,我们将提交一百万个任务,每个任务需要100微秒才能完成。

Callable<String> task = () -> {
    long oneHundredMicroSeconds = 100_000;
    long startedAt = System.nanoTime();
    while (System.nanoTime() - startedAt <= oneHundredMicroSeconds);

    return "Done";
};

var cachedPool = Executors.newCachedThreadPool();
var tasks = IntStream.rangeClosed(1, 1_000_000).mapToObj(i -> task).collect(toList());
var result = cachedPool.invokeAll(tasks);

This is going to create a lot of threads that translate to unreasonable memory usage, and even worse, lots of CPU context switches. Both of these anomalies would hurt the overall performance significantly.

这将会产生大量的线程,转化为不合理的内存使用,更糟糕的是,大量的CPU上下文切换。这两种反常现象都会极大地损害整体性能。

Therefore, we should avoid this thread pool when the execution time is unpredictable, like IO-bound tasks.

因此,当执行时间不可预测时,我们应该避免使用这个线程池,比如IO绑定的任务。

3. Fixed Thread Pool

3.固定线程池

Let’s see how fixed thread pools work under the hood:

让我们看看固定线程池是如何在引擎盖下工作的。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 
      new LinkedBlockingQueue<Runnable>());
}

As opposed to the cached thread pool, this one is using an unbounded queue with a fixed number of never-expiring threads. Therefore, instead of an ever-increasing number of threads, the fixed thread pool tries to execute incoming tasks with a fixed amount of threads. When all threads are busy, then the executor will queue new tasks.  This way, we have more control over our program’s resource consumption.

与缓存线程池相比,这个线程池使用的是一个无界的队列,有固定数量的永不过期的线程。因此,固定线程池不是不断增加的线程数量,而是试图用固定数量的线程执行传入的任务。当所有的线程都很忙时,那么执行者就会排队等待新的任务。 这样一来,我们就能更多地控制程序的资源消耗。

As a result, fixed thread pools are better suited for tasks with unpredictable execution times.

因此,固定线程池更适合于执行时间不可预测的任务。

4. Unfortunate Similarities

4.不幸的相似之处

So far, we’ve only enumerated the differences between cached and fixed thread pools.

到目前为止,我们只列举了缓存线程池和固定线程池之间的区别。

All those differences aside, they’re both use AbortPolicy as their saturation policy. Therefore, we expect these executors to throw an exception when they can’t accept and even queue any more tasks.

抛开这些差异,它们都使用AbortPolicy作为它们的saturation policy因此,我们希望这些执行器在无法接受甚至无法排队更多任务时抛出一个异常。

Let’s see what happens in the real world.

让我们看看在现实世界中会发生什么。

Cached thread pools will continue to create more and more threads in extreme circumstances, so, practically, they will never reach a saturation point. Similarly, fixed thread pools will continue to add more and more tasks in their queue. Therefore, the fixed pools also will never reach a saturation point.

缓存线程池在极端情况下会继续创建越来越多的线程,所以,实际上,它们永远不会达到饱和点。同样地,固定线程池将继续在其队列中增加越来越多的任务。因此,固定池也将永远不会达到饱和点

As both pools won’t be saturated, when the load is exceptionally high, they will consume a lot of memory for creating threads or queuing tasks. Adding insult to the injury, cached thread pools will also incur a lot of processor context switches.

由于这两个池子不会饱和,当负载特别高时,它们将消耗大量的内存用于创建线程或排队任务。雪上加霜的是,缓存的线程池也将产生大量的处理器上下文切换。

Anyway, to have more control over resource consumption, it’s highly recommended to create a custom ThreadPoolExecutor:

无论如何,为了对资源消耗有更多的控制,强烈建议创建一个自定义的ThreadPoolExecutor

var boundedQueue = new ArrayBlockingQueue<Runnable>(1000);
new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy());

Here, our thread pool can have up to 20 threads and can only queue up to 1000 tasks. Also, when it can’t accept any more load, it will simply throw an exception.

在这里,我们的线程池最多可以有20个线程,最多只能排入1000个任务。另外,当它不能再接受任何负载时,它将简单地抛出一个异常。

5. Conclusion

5.总结

In this tutorial, we had a peek into the JDK source code to see how different Executors work under the hood. Then, we compared the fixed and cached thread pools and their use-cases.

在本教程中,我们偷看了JDK的源代码,看看不同的执行器s是如何在引擎罩下工作的。然后,我们比较了固定和缓存线程池及其使用情况。

In the end, we tried to address the out-of-control resource consumption of those pools with custom thread pools.

最后,我们试图用自定义线程池来解决这些池子的资源消耗失控的问题。