1.背景我们使用线程池来有效地使系统工作负载与系统资源保持一致。这个系统工作负载应该是一个可以独立运行的任务。例如,Web应用程序的每个Http请求都可以属于这一类,我们可以处理每个请求而无需考虑另一个Http请求。我们期望我们的应用程序具有良好的吞吐量和良好的响应能力。为此,首先我们应该将我们的应用程序工作划分为独立的任务,然后我们应该以有效利用系统资源(如CPU、RAM(利用率))的方式运行这些任务。通过使用线程池,目标是在高效使用系统资源的同时运行这些单独的任务。如果忽略磁盘和网络,给定一个CPU资源,顺序执行A和B总是比通过时间分片“同时”执行A和B更快,这是计算的基本规律。一旦线程数超过CPU内核数,添加更多线程会变得更慢,而不是更快。例如,在8核服务器上,理想情况下将线程数设置为8将提供最佳性能,超出此数量的任何内容都会由于上下文切换的开销而开始变慢。但是Disk和Network在实际情况下是不能忽略的。2、Java原生线程池关于线程池的详细实现原理:线程池的基本原理,线程池的生命周期管理,具体的设计等等,基本上你能想到的都有,很详细;Java的Executors类提供了一些不同类型的线程池;staticExecutorServicenewSingleThreadExecutor()创建一个使用单个工作线程在无界队列中运行的执行器。staticExecutorServicenewCachedThreadPool()newCachedThreadPool创建一个可缓存的线程池。如果线程池长度超过处理需要,可以灵活回收空闲线程。如果没有可回收线程,则会创建一个新线程。实现原理将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用synchronousQueue(无界),即任务来了就创建线程运行,线程空闲超过60秒就销毁线程。它是一个无限大小的线程池。比如适合执行很多短期异步任务的小程序,或者负载轻的服务器。staticExecutorServicenewFixedThreadPool(intnThreads)创建一个固定长度的线程池,可以控制最大并发线程数,超出的线程会在队列中等待。创建的线程池corePoolSize和maximumPoolSize值相等,它使用LinkedBlockingQueue(无界队列)。适用于需要限制当前线程数以满足资源管理要求的应用场景。staticSc??heduledExecutorServicenewScheduledThreadPool(intcorePoolSize)创建一个支持定时和周期性任务执行的定长线程池。适用于多个后台线程执行周期性任务,限制后台线程数量以满足资源管理需求的应用场景。newSingleThreadExecutor(),因为这个池只有1个线程,所以我们提交给这个线程池的每个任务都是顺序工作的,没有并发性,如果我们有可以独立运行的任务,这个配置在我们应用的吞吐量和响应能力上是不好的。newCachedThreadPool(),因为这个池为每个提交到池中的任务创建一个新线程或使用一个现有线程。对于某些场景(例如,如果我们的任务是短暂的,这种池的使用可能对我们的独立任务有意义。如果我们的任务不是短暂的,使用这种线程池会导致在线程上创建很多线程application。如果我们创建的线程多于阈值,那么我们就不能有效地使用CPU资源,因为大部分CPU时间都花在线程或上下文切换上,而不是真正的工作。这再次导致我们的应用程序响应速度和吞吐量下降.我们需要一个线程池newFixedThreadPool(intnThreads),我们应该选择理想的大小来增加我们的应用程序吞吐量和响应能力(我假设我们有可以独立运行的任务)。重点是不要选择太多也不要太小.前者导致CPU花费太多时间进行线程切换而不是真正的任务,也会导致内存使用过多的问题,后者导致CPU在我们有应该处理的任务时空闲。??:只有当任务类型相同且相互独立时,线程池的效率才达到最佳 例1:(饥饿或死锁)在单线程池中,正在执行的任务被阻塞等待队列中的一个任务完成 例2:(饥饿或死锁)线程池不够大时,通过栅栏机制协调多个任务时 例3:(饥饿)由于其他资源的隐式限制,每个任务需要使用有限的数据库连接资源,所以不管线程池有多大,都会表现出和连接资源一样大。每当提交依赖的Executor任务时,很明显可能会发生线程“饥饿”死锁,因此需要在Executor的代码或配置文件中记录线程池的大小限制或配置限制。下面的代码给出了一个死锁产生的例子。包com.flydean;导入org.junit.Test;导入java.util.concurrent.*;公共类ThreadPoolDeadlock{ExecutorServiceexecutorService=Executors.newSingleThreadExecutor();publicclassRenderPageTaskimplementsCallable{publicStringcall()throwsException{Future页眉,页脚;header=executorService.submit(()->{return"Loadheader";});footer=executorService.submit(()->{return"Loadfooter";});返回header.get()+footer.get();}}publicvoidsubmitTask(){executorService.submit(newRenderPageTask());}}死锁分析:RenderPageTask任务中有2个子任务,分别是“loadingHeader”和“LoadingFooter”。当提交RenderPageTask任务时,线程池中实际加入了3个任务,但是由于线程池是单线程池,所以同时只会执行1个任务,2个子任务会阻塞在线程池中。RenderPageTask任务会因为无法返回而一直阻塞,不会释放线程资源让子线程执行。这会导致线程饥饿死锁。2.1.2.长时间运行任务的线程池大小 应该超过长时间运行任务的数量。否则,线程池中的所有线程都可能为长时间运行的任务服务,导致其他短时间运行的任务也阻塞,导致性能不佳下降缓解策略:限制任务等待资源的时间。如果等待超时,则可以将该任务标记为失败,然后可以中止该任务或将其返回到队列中以供后续执行。这样一来,无论任务的最终结果是否成功,该方法都保证了任务始终可以继续执行,并腾出线程去执行一些可以更快完成的任务。例如,Thread.join、BlockingQueue.put、CountDownLatch.await和Selector.select等。2.1.3长短期混合情况混合长时间运行和极短事务的系统通常最难调整任何连接池。在这些情况下,创建两个池实例可以很好地工作(例如,一个用于长时间运行的作业,另一个用于“实时”查询或将长时间运行的任务提交给单线程Executor,或将多个长时间运行的任务提交给只有几个线程的线程池)。2.1.4.使用ThreadLocal的任务只有当线程局部值的生命周期被限制在任务的生命周期内时,在线程池的线程中使用ThreadLocal才有意义,而不应该将ThreadLocal作为线程中的值线程池传输。阿里的TransmittableThreadLocal让线程池在提交任务的时候传递ThreadLocal的值。2.2.线程池大小设置线程池大小调优计算的本质是:计算瓶颈资源的处理时间与CPU任务运行时间的比值关系。当线程数=(瓶颈资源处理时间/cpu时间)+1时,即瓶颈资源阻塞当前线程时,仍有“恰到好处”的其他线程数处理类似任务,刚好可以达到此时的CPU资源与瓶颈资源和谐共处的美态。2.2.1.相关概念I/O-bound(I/O-bound)I/Obound是指系统的CPU性能远远优于硬盘/内存。此时大部分系统操作是CPU等待I/O(硬盘/内存)读/写,此时CPULoading不高。Computation-intensive(CPU-bound)CPUbound是指系统的硬盘/内存性能远优于CPU。此时系统运行,大部分时间是CPULoading100%,CPU需要读写I/O(硬盘/内存),I/O可以在短时间内完成,但是CPU还有很多计算要处理,CPULoading很高。在多程序系统中,将大部分时间花在计算、逻辑判断和其他CPU动作上的程序称为CPUbound。例如,一个计算pi到小于1,000位小数的程序在执行过程中大部分时间都花在了三角函数和平方根计算上,这是一个CPU密集型任务;此外,加密、解密、压缩和解压、搜索和排序等服务也是CPU密集型服务。TPS(TransactionsPerSecond)概念:服务器每秒处理的事务数。一件事是用户发起查询请求,服务器响应的时间。划重点,对于单个界面,TPS可以认为等同于QPS,比如访问'order.html'页面,就是一个TPS。访问'order.html'页面可能会请求3个服务器(比如调用css、js、order接口),实际上会产生3个QPS。所以综上所述,当针对单个接口时,TPS=QPS,否则,TPS取决于实际请求数。QPS在一定的并发度下,服务器每秒能处理多少个请求,通常我们需要计算的是在资源被充分利用且不过量的前提下合理的QPS。最佳线程数是刚好消耗服务器瓶颈资源的临界线程数。备注:瓶颈资源可以是CPU、内存、锁资源或IO资源。时间差异。这个过程包括DNS解析、网络数据传输、服务器计算、网络数据返回。服务器计算时间可以细分为:WebServer响应时间;应用服务器响应时间;CPU执行时间;线程等待时间(DB,storage,rpccalls等IOwaiting,sleep,wait等引起的)2.2.2.调优计算分析考虑应用类型分析线程池调优:对于混合应用:CPU核数*(1/CPU利用率)=CPU核数*(1+(I/O耗时/CPU耗时))对于cpu-密集型应用:在有N个处理器的系统上,当线程池的大小为N+1时,通常可以达到最佳效率。(+1原因:即使计算密集型线程偶尔会因为缺失故障或其他原因而挂起,这个额外的线程可以确保CPU时钟周期不会被浪费。)对于io密集型应用程序:一般来说,如果有IO,那么肯定是W/C>1(阻塞时间一般是计算时间的很多倍),但是需要考虑系统内存有限(每个线程都需要内存空间),这里需要去服务端测试具体多少线程合适(CPU配比、线程数、总耗时、内存消耗)。初始情况下,保守点取1,即Nthreads=Ncpu*(1+1)=2Ncpu。考虑响应时间的角度,分析线程池调优的最佳线程数=(线程总时间/瓶颈资源时间)*瓶颈资源最佳线程数QPS=瓶颈资源最佳线程数*1000/线程RT??,其中RTisResponseTime例子:在一个4cpu的服务器上,有这样一个线程:预处理数据需要15??ms,调用rpc需要80ms,解析结果需要5ms。如果CPU计算是瓶颈资源,那么最优线程数=((RT)/RT中CPU执行时间)*CPU数=((15+80+5)/(15+5))*4cpu=20如果调用rpc的方法加了同步锁,而这个锁是瓶颈资源,那么由于同步锁是串行资源,并行数为1,所以最优线程数=(locktimeinRT/RT)*1=((15+80+5)/80)*1seriallock=1.25同理,以xx为瓶颈资源为例,计算最优线程数。最佳线程数=(RT/xx瓶颈资源时间)*xx瓶颈资源的并行线程数缩短瓶颈资源消耗的RT可以提高QPS,但缩短其他RT(即非瓶颈资源)则不能。QPS最终取决于瓶颈资源。考虑线程饥饿锁:计算池大小以避免死锁是一个相当简单的资源分配公式:池大小=Tnx(Cm-1)+1其中Tn是最大线程数,Cm是同时保持的最大连接数通过一个线程号。例如,假设三个线程(Tn=3)每个都需要四个连接来执行某个任务(Cm=4)。确保永远不会发生死锁所需的池大小是:poolsize=3x(4-1)+1=10另一个例子,最多有八个线程(Tn=8),每个线程需要三个连接来执行一个任务(Cm=3)。保证永不死锁所需的池大小为:Poolsize=8x(3-1)+1=17总结:由于jdk原有的线程池在实际使用中存在各种问题,合理配置了个数线程池的数量确实是一件很难控制的事情。目前各大厂商对线程池的封装都比较好,不需要商家手动配置线程数,但是可以真正理解线程池调优。计算原理,根据实际业务情况进行计算,确实是一件比较难的事情。