Java并发(八)计算线程池最佳线程数

目录

  1、理论分析java

  2、实际应用算法

 

为了加快程序处理速度,咱们会将问题分解成若干个并发执行的任务。而且建立线程池,将任务委派给线程池中的线程,以便使它们能够并发地执行。在高并发的状况下采用线程池,能够有效下降线程建立释放的时间花销及资源开销,如不使用线程池,有可能形成系统建立大量线程而致使消耗完系统内存以及“过分切换”(在JVM中采用的处理机制为时间片轮转,减小了线程间的相互切换) 。数据库

可是有一个很大的问题摆在咱们面前,即咱们但愿尽量多地建立任务,但因为资源所限咱们又不能建立过多的线程。那么在高并发的状况下,咱们怎么选择最优的线程数量呢?选择原则又是什么呢?编程

1、理论分析

关于如何计算并发线程数,有两种说法。服务器

第一种,《Java Concurrency in Practice》即《java并发编程实践》8.2节 170页网络

对于计算密集型的任务,一个有Ncpu个处理器的系统一般经过使用一个Ncpu + 1个线程的线程池来得到最优的利用率(计算密集型的线程刚好在某时由于发生一个页错误或者因其余缘由而暂停,恰好有一个“额外”的线程,能够确保在这种状况下CPU周期不会中断工做)。对于包含了 I/O和其余阻塞操做的任务,不是全部的线程都会在全部的时间被调度,所以你须要一个更大的池。为了正确地设置线程池的长度,你必须估算出任务花在等待的时间与用来计算的时间的比率;这个估算值没必要十分精确,并且能够经过一些监控工具得到。你还能够选择另外一种方法来调节线程池的大小,在一个基准负载下,使用 几种不一样大小的线程池运行你的应用程序,并观察CPU利用率的水平。多线程

  给定下列定义:并发

  Ncpu = CPU的数量高并发

  Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1工具

  W/C = 等待时间与计算时间的比率

  为保持处理器达到指望的使用率,最优的池的大小等于:

  Nthreads = Ncpu x Ucpu x (1 + W/C)

  你可使用Runtime来得到CPU的数目:

int N_CPUS = Runtime.getRuntime().availableProcessors();

固然,CPU周期并非惟一你可使用线程池管理的资源。其余能够约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库链接等。计算这些类型资源池的大小约束很是简单:首先累加出每个任务须要的这些资源的总童,而后除以可用的总量。所 得的结果是池大小的上限。

当任务须要使用池化的资源时,好比数据库链接,那么线程池的长度和资源池的长度会相互影响。若是每个任务都须要一个数据库链接,那么链接池的大小就限制了线程池的有效大小;相似地,当线程池中的任务是链接池的惟一消费者时,那么线程池的大小反而又会限制了链接池的有效大小。

如上,在《Java Concurrency in Practice》一书中,给出了估算线程池大小的公式:

  Nthreads = Ncpu x Ucpu x (1 + W/C),其中

  Ncpu = CPU核心数

  Ucpu = CPU使用率,0~1

  W/C = 等待时间与计算时间的比率

第二种,《Programming Concurrency on the JVM Mastering》即《Java 虚拟机并发编程》2.1节 12页

为了解决上述难题,咱们但愿至少能够建立处理器核心数那么多个线程。这就保证了有尽量多地处理器核心能够投入到解决问题的工做中去。经过下面的代码,咱们能够很容易地获取到系统可用的处理器核心数:

Runtime.getRuntime().availableProcessors();

因此,应用程序的最小线程数应该等于可用的处理器核数。若是全部的任务都是计算密集型的,则建立处理器可用核心数那么多个线程就能够了。在这种状况下,建立更多的线程对程序性能而言反而是不利的。由于当有多个仟务处于就绪状态时,处理器核心须要在线程间频繁进行上下文切换,而这种切换对程序性能损耗较大。但若是任务都是IO密集型的,那么咱们就须要开更多的线程来提升性能。

当一个任务执行IO操做时,其线程将被阻塞,因而处理器能够当即进行上下文切换以便处理其余就绪线程。若是咱们只有处理器可用核心数那么多个线程的话,则即便有待执行的任务也没法处理,由于咱们已经拿不出更多的线程供处理器调度了。

若是任务有50%的时间处于阻塞状态,则程序所需线程数为处理器可用核心数的两倍。 若是任务被阻塞的时间少于50%,即这些任务是计算密集型的,则程序所需线程数将随之减小,但最少也不该低于处理器的核心数。若是任务被阻塞的时间大于执行时间,即该任务是IO密集型的,咱们就须要建立比处理器核心数大几倍数量的线程。

咱们能够计算出程序所需线程的总数,总结以下:

  线程数 = CPU可用核心数/(1 - 阻塞系数),其中阻塞系数的取值在0和1之间。

  计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。一个彻底阻塞的任务是注定要挂掉的,因此咱们无须担忧阻塞系数会达到1。

为了更好地肯定程序所需线程数,咱们须要知道下面两个关键参数:

  • 处理器可用核心数;
  • 任务的阻塞系数;

第一个参数很容易肯定,咱们甚至能够用以前的方法在运行时查到这个值。但肯定阻塞系数就稍微困难一些。咱们能够先试着猜想,抑或采用一些性能分析工具或java.lang.management API来肯定线程花在系统IO操做上的时间与CPU密集任务所耗时间的比值。

如上,在《Programming Concurrency on the JVM Mastering》一书中,给出了估算线程池大小的公式:

  线程数 = Ncpu /(1 - 阻塞系数)

 

对于说法一,假设CPU 100%运转,即撇开CPU使用率这个因素,线程数 = Ncpu x (1 + W/C)。

如今假设将方法二的公式等于方法一公式,即Ncpu /(1 - 阻塞系数)= Ncpu x (1 + W/C),推导出:阻塞系数 = W / (W + C),即阻塞系数 = 阻塞时间 /(阻塞时间 + 计算时间),这个结论在方法二后续中获得印证,以下:

因为对Web服务的请求大部分时间都花在等待服务器响应上了,因此阻塞系数会至关高,所以程序须要开的线程数多是处理器核心数的若干倍。假设阻塞系数是0.9,即每一个任务90%的时间处于阻塞状态而只有10%的时间在干活,则在双核处理器上咱们就须要开20个线程(使用第2.1节的公式计算)。若是有不少只股票要处理的话,咱们能够在8核处理器上开到80个线程来处理该任务。

因而可知,说法一和说法二实际上是一个公式。

2、实际应用

那么实际使用中并发线程数如何设置呢?咱们先看一道题目:

假设要求一个系统的TPS(Transaction Per Second或者Task Per Second)至少为20,而后假设每一个Transaction由一个线程完成,继续假设平均每一个线程处理一个Transaction的时间为4s。那么问题转化为:

 

如何设计线程池大小,使得能够在1s内处理完20个Transaction?

 

计算过程很简单,每一个线程的处理能力为0.25TPS,那么要达到20TPS,显然须要20/0.25=80个线程。

这个理论上成立的,可是实际状况中,一个系统最快的部分是CPU,因此决定一个系统吞吐量上限的是CPU。加强CPU处理能力,能够提升系统吞吐量上限。在考虑时须要把CPU吞吐量加进去。

分析以下(咱们以说法一公式为例):

  Nthreads = Ncpu x (1 + W/C)

即线程等待时间所占比例越高,须要越多线程。线程CPU时间所占比例越高,须要越少线程。这就能够划分红两种任务类型:

IO密集型:通常状况下,若是存在IO,那么确定W/C > 1(阻塞耗时通常都是计算耗时的不少倍),可是须要考虑系统内存有限(每开启一个线程都须要内存空间),这里须要在服务器上测试具体多少个线程数适合(CPU占比、线程数、总耗时、内存消耗)。若是不想去测试,保守点取1便可,Nthreads = Ncpu x (1 + 1) = 2Ncpu。这样设置通常都OK。

计算密集型:假设没有等待W = 0,则W/C = 0。 Nthreads = Ncpu

根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提升系统吞吐量,就须要从“系统短板”(好比网络延迟、IO)着手:

  • 尽可能提升短板操做的并行化比率,好比多线程下载技术;
  • 加强短板能力,好比用NIO替代IO;

第一条能够联系到Amdahl定律,这条定律定义了串行系统并行化后的加速比计算公式:

加速比 = 优化前系统耗时 / 优化后系统耗时

加速比越大,代表系统并行化的优化效果越好。Addahl定律还给出了系统并行度、CPU数目和加速比的关系,加速比为Speedup,系统串行化比率(指串行执行代码所占比率)为F,CPU数目为N:

Speedup <= 1 / (F + (1-F)/N)

当N足够大时,串行化比率F越小,加速比Speedup越大。

这时候又抛出是否线程池必定比但线程高效的问题?

答案是否认的,好比Redis就是单线程的,但它却很是高效,基本操做都能达到十万量级/s。从线程这个角度来看,部分缘由在于:

  • 多线程带来线程上下文切换开销,单线程就没有这种开销;
  • 锁;

固然“Redis很快”更本质的缘由在于: 

Redis基本都是内存操做,这种状况下单线程能够很高效地利用CPU。而多线程适用场景通常是:存在至关比例的IO和网络操做。

总的来讲,应用状况不一样,采起多线程/单线程策略不一样;线程池状况下,不一样的估算,目的和出发点是一致的。

至此结论为:

IO密集型 = 2Ncpu(能够测试后本身控制大小,2Ncpu通常没问题)(常出现于线程中:数据库数据交互、文件上传下载、网络数据传输等等)

计算密集型 = Ncpu(常出现于线程中:复杂算法)

固然说法一中还有一种说法:

对于计算密集型的任务,一个有Ncpu个处理器的系统一般经过使用一个Ncpu + 1个线程的线程池来得到最优的利用率(计算密集型的线程刚好在某时由于发生一个页错误或者因其余缘由而暂停,恰好有一个“额外”的线程,能够确保在这种状况下CPU周期不会中断工做)。

即,计算密集型 = Ncpu + 1,可是这种作法致使的多一个CPU上下文切换是否值得,这里不考虑。读者可本身考量。

相关文章
相关标签/搜索