当涉及 线程池 实现时,Java标准库提供了很多选择。在这些实现中,固定线程池和缓存线程池非常普遍。
缓存线程池newCachedThreadPool
Executors.newCachedThreadPool 缓存线程池是从零个线程开始,并且可能会增长为具有 Integer.MAX_VALUE个 线程。实际上,缓存线程池的唯一限制是可用的系统资源。
为了更好地管理系统资源,缓存的线程池将删除闲置一分钟的线程。为了更好地管理系统资源,缓存的线程池将删除闲置一分钟的线程。
假设有一个新任务进入。如果队列中有一个空闲线程正在等待,则任务生产者将任务移交给该线程。否则,由于队列始终已满,执行程序将创建一个新线程来处理该任务。
缓存线程池配置会在很短的时间内缓存线程(因此命名),以将其重用于其他任务 。因此,当我们处理 相当数量的短期任务时 ,它最有效。
这里的关键是“合理”和“短暂”。为了澄清这一点,让我们评估一个缓存池不太适合的情况。在这里,我们将提交100万个任务,每个任务需要100微秒的时间来完成:
allable<String> task = () -> { <b>long</b> oneHundredMicroSeconds = 100_000; <b>long</b> startedAt = System.nanoTime(); <b>while</b> (System.nanoTime() - startedAt <= oneHundredMicroSeconds); <b>return</b> <font>"Done"</font><font>; }; <b>var</b> cachedPool = Executors.newCachedThreadPool(); <b>var</b> tasks = IntStream.rangeClosed(1, 1_000_000).mapToObj(i -> task).collect(toList()); <b>var</b> result = cachedPool.invokeAll(tasks); </font>
这将创建许多线程,这些线程会转换为不合理的内存使用,甚至更糟的是,还会有许多CPU上下文切换。这两个异常都会严重损害整体性能。
因此, 当执行时间不可预测时(如IO绑定任务),我们应避免使用缓存线程池。
固定线程池newFixedThreadPool
与缓存线程池相反,该线程正在使用具有固定数量的永不过期线程的无界队列。因此,固定线程池不是使用数量不断增加的线程,而是尝试使用固定数量的线程执行传入的任务。当所有线程都忙时,执行程序将对新任务进行排队。这样,我们可以更好地控制程序的资源消耗。
因此,固定线程池更适合执行时间无法预测的任务。
共同点
除了所有这些差异,它们都使用 AbortPolicy 作为 饱和策略 。因此,我们希望这些执行器在无法接受甚至无法排队任务时抛出异常。
让我们看看现实世界中发生了什么。
在极端情况下,缓存线程池将继续创建越来越多的线程,因此,实际上,它们永远不会达到饱和点。同样,固定线程池将继续在其队列中添加越来越多的任务。因此,固定池也永远不会达到饱和点。
由于两个池都不会饱和,因此当负载异常高时,它们将消耗大量内存来创建线程或排队任务。更糟的是,缓存的线程池也将导致很多处理器上下文切换。
无论如何,为了更好地控制资源消耗,强烈建议创建一个自定义 ThreadPoolExecutor :
<b>var</b> boundedQueue = <b>new</b> ArrayBlockingQueue<Runnable>(1000); <b>new</b> ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, <b>new</b> AbortPolicy());
在这里,我们的线程池最多可以有20个线程,最多只能排队1000个任务。同样,当它不能再接受任何负载时,它只会抛出一个异常。