首先,在Java的类库中,提供了几种常用的线程池类型。
Executors.newCachedThreadPool(缓存线程池):该线程池根据任务数量动态地创建线程,线程池的大小会根据任务的多少自动调整。
Executors.newScheduledThreadPool(定时线程池):是ScheduledExecutorService接口的实现类。该线程池可以按照固定的时间间隔或延迟执行任务。
Executors.newFixedThreadPool(固定线程池):该线程池维护固定数量的线程,任务提交后会立即执行。如果所有线程都处于忙碌状态,任务会在队列中等待。
Executors.newSingleThreadExecutor(单线程线程池):该线程池只包含一个线程,所有任务按照顺序执行。
Executors.newWorkStealingPool(工作窃取线程池):JDK 8引入,内部构建一个ForkJoinPool,创建持有足够线程来支持给定的并行度的线程池。该线程池使用多个队列,每个线程维护一个自己的队列。当一个线程完成自己队列中的任务后,会从其他线程的队列中窃取任务执行,因此构造方法中把CPU数量设置为默认的并行度。
这些线程池类型都是通过java.util.concurrent.Executors类提供的静态方法创建的。这些默认提供的线程池还是阿里建议使用ThreadPoolExecutor类创建线程池,其里面包含了很多参数。有一些默认参数值或需要自定义参数值,让我们先一起认识一下线程池参数:
corePoolSize:线程池核心线程个数
maximumPoolSize:线程池最大线程数量
keepAliveTime:存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则这些闲置的线程能存活的最大时间。
TimeUnit:存活时间的时间单位。
workQueue:用于保存等待执行的任务的阻塞队列。ThreadFactory:创建线程的工厂。可以自定义线程的一些属性,比如名称或者守护线程等
RejectedExecutionHandler:饱和策略。当队列满并且线程个数达到maxmumPoolSize后采取的策略。
在使用线程池时,该如何配置线程池参数,有说经验值法、最佳线程数目算法、Java并发编程实践提供的计算方法、Java虚拟机并发编程提供的计算方法、微信公众号的一篇文章(如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答)等。我们一起探讨一下这个问题。
1.经验值
配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?
IO密集型:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。
CPU密集型:非常复杂的调用,循环次数很多,或者递归调用层次很深等。
IO密集型配置线程数经验值是:2N,其中N代表CPU核数。
CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。
1.1.如何获取N的值?
int availableProcessors = Runtime.getRuntime().availableProcessors();
1.2.IO密集型的经验值为什么是2N?
对于IO密集型应用,如果使用Java并发编程实践提供的计算方法(Nthreads=Ncpu*Ucpu*(1+W/C)),假定所有的操作时间几乎都是IO操作耗时,那么W/C的值就为1,那么对应的线程数确实为2*N。
1.3.CPU密集型的经验值为什么是N+1?
对于CPU密集型应该,还是使用Java并发编程实践提供的计算方法(Nthreads=Ncpu*Ucpu*(1+W/C)),假定等待时间趋近于0,就是CPU利用率达到100%,那么线程数就是CPU核心数,但是计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个"额外"的线程,可以确保在这种情况下CPU周期不会中断工作。所以N+1确实是个经验值。
1.4.那么问题来了,混合型(既包含IO密集型,又包含CPU密集型)的如何配置线程数?
混合型如果IO密集型,和CPU密集型的执行时间相差不太大,可以拆分开,以便于更好配置。如果执行时间相差太大,优化的意义不大,比如IO密集型耗时60s,CPU密集型耗时1s。
2.最佳线程数目算法
除了经验值之外,其实还提供了计算公式:
最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目
很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
3.Java并发编程实践
Ncpu=CPU的数量
Ucpu=目标CPU的使用率,0<=Ucpu<=1
W/C=等待时间与计算之间的比值
要是处理器达到期望的使用率,线程池的最优大小等于
Nthreads=Ncpu*Ucpu*(1+W/C)
4.Java虚拟机并发编程
线程数=CPU可用核心数/(1-阻塞系数),其中阻塞系数的取值在0和1之间。
计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。
阻塞系数=阻塞时间/(阻塞时间+计算时间)
以上线程池参数的设置只是经验或者理想情况下得出的结论,其要正确地设置线程池的大小,这是非常难实现的。或许可以通过一些分析或监控工具,调节线程池的大小,并且在某个基准负载下,分别设置不同大小的线程池来运行应用程序,观察CPU利用率的水平,来达到一个比较平衡的线程池大小。