既然创建或销毁线程存在一定的开销,所以利用线程池技术来提高系统资源利用效率,并简化线程管理,已经是非常成熟的选择。
典型回答
通常开发者都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的ExecutorService类型或者不同的初始参数。Executors目前提供了5种不同的线程池创建配置:
扩展知识
1、Executor框架概述
首先我们来看看Executor框架的基本组成,参考以下类图:
Executor是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点可以体会其定义的唯一方法。
void execute(Runnable command);
Executor的设计是源于Java早期线程API使用的教训,开发者在实现应用逻辑时,被太多线程创建、调度等不相关细节所打扰。导致开发效率低下,质量也难以保证。
ExecutorService则更加完善。不仅提供service的管理功能,比如shutdown等方法;也提供了更加全面的提交任务机制。如返回Future而不是void的submit方法。
Future submit(Callable task);
注意,这个例子输入的是Callable,它解决了Runnable无法返回结果的困扰。
Java标准类库提供了几种基础实现,比如ThreadPoolExecutor、ScheduledThreadPoolExecutor、ForkJoinPool。这些线程池的设计特点在于其高度的可调节性和灵活性,以尽量满足复杂多变的实际应用场景。
Executors则从简化使用的角度,为我们提供了各种方便的静态工厂方法。
2、ThreadPoolExecutor
理解应用与线程池的交互和线程池的内部工作过程,你可以参考下图:
工作队列负责存储用户提交的各个任务。这个工作队列,可以是容量为0的SynchronousQueue(使用newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用LinkedBlockingQueue。
private final BlockingQueue workQueue;
内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会在闲置一段时间(默认60秒)后结束线程。
private final HashSet workers = new HashSet<>();
线程池的工作线程被抽象为静态内部类Worker,基于AbstractQueuedSynchronizer实现。
ThreadFactory提供上面所需要的创建线程逻辑。
如果任务提交时被拒绝,比如线程池已经处于SHUTDOWN状态,需要为其提供处理逻辑。Java标准库提供了类似ThreadPoolExecutor.AbortPolicy等默认实现,也可以按照实际需要自定义。
从上面的分析,就可以看出线程池的几个基本组成部分,一起都体现在线程池的构造函数中,从字面我们就可以大概猜测到其用意:
3、线程池实践
线程池虽然提供了非常强大、方便的功能,但是也不是银弹,使用不当同样会导致问题。我这里介绍些典型情况,经过前面的分析,很多方面可以自然地推导出来。
4、线程池大小的选择策略
上面已经介绍过,线程池大小不合适,太多或太少,都会导致麻烦,所以我们需要去考虑一个合适的线程池大小。虽然不能完全确定,但是有一些相对普适的规则和思路。
线程数 = CPU核数 * (1 + 平均等待时间 / 平均工作时间)
这些时间并不能精准预测,需要根据采样或者概要分析等方式进行计算,然后在实际中验证和调整。
另外,在实际工作中,不要把解决问题的思路全部指望到调整线程池上,很多时候架构上的改变更能解决问题。