Executors类的newFixedThreadPool方法创建了一个固定线程数的executorService 。与newSingleThreadExecutor方法不同,我们指定池中需要多少个线程。在引擎盖下,调用以下代码:
new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue
());
corePoolSize (执行器服务启动时准备好(启动)的线程数)和maximumPoolSize (执行器服务可以创建的最大线程数)参数接收相同的值——传递给newFixedThreadPool(nThreads)的线程数)。我们可以用完全相同的方式传递我们自己的ThreadFactory实现。
好吧,让我们看看为什么我们需要这样一个ExecutorService。
这是具有固定数量 (n) 线程的ExecutorService的逻辑:
- 最多有 n 个线程将处于活动状态以处理任务。
- 如果提交的任务超过 n 个,它们将被保留在队列中,直到线程空闲为止。
- 如果其中一个线程失败并终止,将创建一个新线程来代替它。
- 池中的任何线程都处于活动状态,直到池关闭。
例如,想象一下在机场等待通过安检。每个人都排成一排,直到安检前,乘客被分配到所有工作检查站。如果其中一个检查点出现延迟,则队列将仅由第二个检查点处理,直到第一个检查点空闲为止。如果一个检查站完全关闭,那么另一个检查站将被打开以取代它,乘客将继续通过两个检查站进行处理。
我们会立即注意到,即使条件是理想的——承诺的 n 个线程稳定工作,并且以错误结束的线程总是被替换(在真实的机场中有限的资源不可能实现的东西)——系统仍然有几个不愉快的特性,因为在任何情况下都不会有更多的线程,即使队列增长的速度快于线程处理任务的速度。
我建议实际了解ExecutorService如何使用固定数量的线程。让我们创建一个实现Runnable 的类。此类的对象代表我们对ExecutorService 的任务。
public class Task implements Runnable {
int taskNumber;
public Task(int taskNumber) {
this.taskNumber = taskNumber;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Processed user request #" + taskNumber + " on thread " + Thread.currentThread().getName());
}
}
在run()方法中,我们阻塞线程 2 秒,模拟一些工作负载,然后显示当前任务的编号和执行任务的线程的名称。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 30; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
首先,在main方法中,我们创建一个ExecutorService并提交 30 个任务以供执行。
在 pool-1-thread-1 线程上
处理的用户请求 #0 在 pool-1-thread-3 线程上处理的用户请求 #2
在 pool- 上处理的用户请求 #5 1-thread-3 线程
在 pool-1-thread-2 线程上处理的用户请求 #3
在 pool-1-thread-1 线程上处理的用户请求 #4 在 pool-1
-thread-1 线程上处理的用户请求 #8
处理的用户pool-1-thread-3 线程上的请求 #6 在
pool-1-thread-2 线程上处理的用户请求 #7 在 pool-1
-thread-3 线程上处理的用户请求 #10
在 pool-1- 上处理的用户请求 #9 thread-1 thread
在 pool-1-thread-2 线程上处理的用户请求 #11
在 pool-1-thread-3 线程上处理的用户请求 #12
在 pool-1-thread-2 线程上处理的用户请求 #14 在
pool-1-thread-1 线程上处理的用户请求 #13
在 pool-1-thread-3 线程上处理的用户请求 #15
在 pool- 上处理的用户请求 #16 1-thread-2 线程
在 pool-1-thread-1 线程上处理的用户请求 #17
在 pool-1-thread-3 线程上处理的用户请求 #18
在 pool-1-thread-2 线程上处理的用户请求 #19
处理的用户pool-1-thread-1 线程上的请求 #20 在
pool-1-thread-3 线程上处理的用户请求 #21
在 pool-1-thread-2 线程上处理的用户请求 #22
在 pool-1- 上处理的用户请求 #23 thread-1 thread
在 pool-1-thread-2 线程上处理的用户请求 #25
在 pool-1-thread-3 线程上处理的用户请求 #24
在 pool-1-thread-1 线程上处理的用户请求 #26 在
pool-1-thread-2 线程
上处理的用户请求 #27 在 pool-1-thread-3 线程上处理的用户请求 #28
在 pool- 上处理的用户请求 #29 1-thread-1线程
控制台输出向我们展示了任务在被前一个任务释放后如何在不同线程上执行。
现在我们将任务数增加到 100,在提交 100 个任务后,我们将调用 awaitTermination (11, SECONDS)方法。我们传递一个数字和时间单位作为参数。此方法将阻塞主线程 11 秒。然后我们将调用shutdownNow()强制关闭ExecutorService ,而不等待所有任务完成。
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 100; i++) {
executorService.execute(new Task(i));
}
executorService.awaitTermination(11, SECONDS);
executorService.shutdownNow();
System.out.println(executorService);
最后,我们将显示有关 executorService 状态的信息。
这是我们得到的控制台输出:
在 pool-1-thread-3 线程上
处理的用户请求 #2 在 pool-1-thread-2 线程上处理的用户请求 #1
在 pool- 上处理的用户请求 #4 1-thread-3 线程
在 pool-1-thread-2 线程上处理的用户请求 #5
在 pool-1-thread-1 线程
上处理的用户请求 #3 在 pool-1-thread-3 线程上处理的用户请求 #6
处理的用户pool-1-thread-2 线程上的请求 #7 在 pool-
1-thread-1 线程上处理的用户请求 #8 在 pool-
1-thread-3 线程上处理的用户请求 #9 在 pool-1- 上
处理的用户请求 #11 thread-1 thread
在 pool-1-thread-2 线程上处理的用户请求 #10
在 pool-1-thread-1 线程上处理的用户请求 #13
在 pool-1-thread-2 线程上处理的用户请求 #14
在 pool-1-thread-3 线程上处理的用户请求 #12
java.util.concurrent.ThreadPoolExecutor@452b3a41[正在关闭,池大小 = 3,活动线程 = 3 , 排队任务 = 0, 完成任务 = 15]
pool-1-thread-3 线程上已处理的用户请求 #17 在 pool-
1-thread-1 线程上已处理的用户请求 #15 在 pool-1
-thread 上已处理的用户请求 #16 -2线程
接下来是 3 个InterruptedExceptions ,由 3 个活动任务的睡眠方法抛出。
我们可以看到当程序结束时,15 个任务已经完成,但是池中仍有 3 个活动线程没有完成它们的任务执行。在这三个线程上调用了interrupt ()方法,这意味着任务将完成,但在我们的例子中,睡眠方法抛出InterruptedException。我们还看到调用shutdownNow()方法后,任务队列被清空。
所以当使用池中线程数固定的ExecutorService时,一定要记住它是如何工作的。这种类型适用于具有已知恒定负载的任务。
这里还有一个有趣的问题:如果你需要为单线程使用一个执行器,你应该调用哪个方法?newSingleThreadExecutor()或newFixedThreadPool(1)?
两个执行者都会有相同的行为。唯一的区别是newSingleThreadExecutor()方法将返回一个执行程序,该执行程序以后无法重新配置为使用其他线程。
GO TO FULL VERSION