另一种类型的线程池是“缓存的”。这样的线程池和固定线程池一样常用。
顾名思义,这种线程池就是缓存线程。它使未使用的线程在有限的时间内保持活动状态,以便重用这些线程来执行新任务。当我们有一些合理的轻量级工作时,这样的线程池是最好的。
“一些合理的数量”的含义相当广泛,但你应该知道,这样的池并不适合每一个任务数量。例如,假设我们要创建一百万个任务。即使每个都花费很少的时间,我们仍然会使用不合理的资源量并降低性能。当执行时间不可预测时,例如 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-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对象作为参数的重载版本。
GO TO FULL VERSION