另一种类型的线程池是“缓存的”。这样的线程池和固定线程池一样常用。

顾名思义,这种线程池就是缓存线程。它使未使用的线程在有限的时间内保持活动状态,以便重用这些线程来执行新任务。当我们有一些合理的轻量级工作时,这样的线程池是最好的。

“一些合理的数量”的含义相当广泛,但你应该知道,这样的池并不适合每一个任务数量。例如,假设我们要创建一百万个任务。即使每个都花费很少的时间,我们仍然会使用不合理的资源量并降低性能。当执行时间不可预测时,例如 I/O 任务,我们也应该避免使用此类池。

在幕后,使用以下参数调用ThreadPoolExecutor构造函数:

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

以下值作为参数传递给构造函数:

范围 价值
corePoolSize (执行器服务启动时将准备好(启动)多少个线程) 0
maximumPoolSize (一个executor服务可以创建的最大线程数) 整数.MAX_VALUE
keepAliveTime(如果线程数大于corePoolSize ,则释放的线程在被销毁之前将继续存在的时间) 60L
单位(时间单位) 时间单位.SECONDS
workQueue(队列的实现) 新同步队列<Runnable>()

我们可以用完全相同的方式传递我们自己的ThreadFactory实现。

说说同步队列

同步传输的基本思想非常简单但违反直觉(即直觉或常识告诉您这是错误的):当且仅当另一个线程在同时。也就是说,同步队列里面不能有任务,因为只要有新的任务到来,执行线程就已经拿走了任务

当一个新任务进入队列时,如果池中有一个空闲的活动线程,那么它就会选择这个任务。如果所有线程都忙,则创建一个新线程。

缓存池从零线程开始,并可能增长到Integer.MAX_VALUE线程。本质上,缓存线程池的大小仅受系统资源的限制。

为了节省系统资源,缓存线程池会删除空闲一分钟的线程。

让我们看看它在实践中是如何工作的。我们将创建一个模拟用户请求的任务类:

public class Task implements Runnable {
   int taskNumber;

   public Task(int taskNumber) {
       this.taskNumber = taskNumber;
   }

   @Override
   public void run() {
       System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
   }
}

main方法中,我们创建了newCachedThreadPool,然后添加了 3 个任务来执行。这里我们打印我们服务的状态(1)

接下来,我们暂停 30 秒,开始另一个任务,并显示状态(2)

之后,我们将主线程暂停 70 秒,打印状态(3),然后再次添加 3 个任务,并再次打印状态(4)

在我们添加任务后立即显示状态的地方,我们首先添加一个 1 秒的睡眠以获取最新的输出。

ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(1)

        TimeUnit.SECONDS.sleep(30);

        executorService.submit(new Task(3));
        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(2)

        TimeUnit.SECONDS.sleep(70);

            System.out.println(executorService);	//(3)

        for (int i = 4; i < 7; i++) {
            executorService.submit(new Task(i));
        }

        TimeUnit.SECONDS.sleep(1);
            System.out.println(executorService);	//(4)
        executorService.shutdown();

结果如下:

在 pool-1-thread-1 线程上处理的用户请求 #0
在 pool-1-thread-2 线程上
处理的用户请求 #1 在 pool-1-thread-3 线程上处理的用户请求 #2
(1) java.util.concurrent .ThreadPoolExecutor@f6f4d33[Running, pool size = 3, active threads = 0, queued tasks = 0, completed tasks = 3] pool-1-thread-2 thread (2) java.util.concurrent
上处理的用户请求 #3 。
ThreadPoolExecutor@f6f4d33[运行,池大小 = 3,活动线程 = 0,排队任务 = 0,完成任务 = 4]
(3) java.util.concurrent.ThreadPoolExecutor@f6f4d33[运行,池大小 = 0,活动线程 = 0 , 排队任务 = 0, 完成任务 = 4]
在 pool-1-thread-4 线程上处理的用户请求 #4
在 pool-1-thread-5 线程上处理的用户请求 #5

在 pool-1-thread-4 线程(4) java.util.concurrent.ThreadPoolExecutor@f6f4d33[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 7] 上 处理的用户请求 #6

让我们回顾一下每个步骤:

解释
1(完成 3 个任务后) 我们创建了3个线程,在这三个线程上执行了3个任务。
当显示状态时,3 个任务都已完成,线程准备好执行其他任务。
2(暂停 30 秒并执行另一项任务后) 闲置 30 秒后,线程仍处于活动状态并等待任务。
添加另一个任务并在从剩余活动线程池中取出的线程上执行。
没有新线程被添加到池中。
3(70 秒暂停后) 线程已从池中删除。
没有准备好接受任务的线程。
4(再执行 3 个任务后) 收到更多任务后,将创建新线程。这次只有两个线程设法处理了 3 个任务。

好了,现在您已经熟悉了另一种执行程序服务的逻辑。

Executors实用程序类的其他方法类比,newCachedThreadPool方法也有一个以ThreadFactory对象作为参数的重载版本。