我们在使用线程池的时候,有两个疑惑:如果线程池的线程数设置的太多,会导致线程竞争激烈。如果线程数设置得太小,系统将无法充分利用计算机资源。设置不会影响系统性能吗?其实线程池的设置是有方法的,不是简单估计就能确定的。今天我们就来看看有哪些计算方法可以复用,线程池中的参数之间有什么关系?本篇我们慢慢聊。线程池原理在开始优化之前,我们先了解一下线程池的实现原理,这有助于大家更好地理解后面的内容。在HotSpotVM的线程模型中,Java线程与内核线程一对一映射。Java使用线程执行程序时,需要创建内核线程;当Java线程终止时,内核线程也会被回收。因此,Java线程的创建和销毁都会消耗一定的计算机资源,从而增加系统的性能开销。另外,创建大量的线程也会给系统带来性能问题,因为内存和CPU资源会被线程抢占。如果处理不当,会出现内存溢出、CPU占用过载等问题。为了解决以上两类问题,Java提供了线程池的概念。对于线程创建频繁的业务场景,线程池可以创建固定数量的线程,在操作系统底层,轻量级进程会将这些线程映射到内核。线程池可以提高线程复用,也可以固定线程的最大使用量,防止无限创建线程。当程序提交任务需要线程时,会去线程池中查找是否有空闲线程。如果有,则直接使用线程池中的线程进行工作。如果没有,则判断当前创建的线程数是否超过最大值。如果没有超过线程数,就会创建一个新的线程;如果已经超过,则等待队列或直接抛出异常。线程池框架ExecutorJava最初提供了ThreadPool来实现线程池。为了更好的实现用户级的线程调度,更有效的帮助开发者进行多线程开发,Java提供了一套Executor框架。这个框架包括两个核心线程池,ScheduledThreadPoolExecutor和ThreadPoolExecutor。前者用于定时执行任务,后者用于执行提交的任务。既然两个线程池的核心原理是一样的,那我们就重点看看ThreadPoolExecutor类是如何实现线程池的。Executors实现了以下四种ThreadPoolExecutor:Executors使用工厂模型实现的四种线程池。我们在使用的时候,需要结合生产环境中的实际场景。不过我不推荐使用它们,因为选择使用Executors提供的工厂类会忽略很多线程池的参数设置。工厂类一旦选择设置默认参数,很容易调参设置失败,导致性能问题或者资源浪费。建议大家使用ThreadPoolExecutor自定义一套线程池(阿里巴巴也建议不要使用Executors创建线程池,而是使用ThreadPoolExecutor创建线程池)。进入四个工厂类后,我们可以发现除了newScheduledThreadPool类之外,其他类都是使用ThreadPoolExecutor类实现的。可以通过下面的代码简单看一下方法corePoolSize:线程池中的核心线程数maximumPoolSize:线程池中的最大线程数keepAliveTime:当线程数大于核心线程数时,冗余空闲线程存活的最长时间unit:时间单位workQueue:任务队列,用于存放等待任务执行的队列threadFactory:线程工厂,用于创建线程,一般默认为Handler:拒绝策略。当提交的任务过多无法及时处理时,我们可以自定义策略来处理任务。我们还可以通过下图来理解线程池中各个参数之间的关系:通过上图,我们发现线程池对于线程数有两种设置,一种是核心线程数,一种是核心线程数另一个是最大线程数。线程池创建后,默认情况下,线程池中没有线程,有任务时创建线程执行任务。但是有一个例外,就是调用prestartAllCoreThreads()或者prestartCoreThread()方法,可以提前创建与核心线程数相等的线程数。这种方法称为预热,通常用于快速系统。当创建的线程数等于corePoolSize时,提交的任务会加入到设置的阻塞队列中。当队列满时,会创建线程执行任务,直到线程池中的数量等于maximumPoolSize。当线程数已经等于maximumPoolSize时,新提交的任务不能加入等待队列,也不能创建非核心线程直接执行。我们没有给线程池设置拒绝策略,那么线程池会抛出RejectedExecutionException异常,即线程池拒绝接受任务。当线程池中创建的线程数超过设定的corePoolSize时,部分线程处理完任务后,如果等待keepAliveTime后仍然没有新的任务分配给它,那么这个线程就会被回收。线程池在回收线程时,对所谓的“核心线程”和“非核心线程”一视同仁,直到线程池中的线程数等于设置的corePoolSize参数时,回收过程才会停止。即使是corePoolSize线程,在一些非核心业务线程池中,如果线程数长期被占用,也可能影响到核心业务线程池。这时候就需要回收没有分配任务的线程。我们可以通过allowCoreThreadTimeOut设置项来请求线程池:所有没有分配任务的线程包括“核心线程”都会在等待keepAliveTime时间后被回收。我们可以通过下图来理解线程池的线程分配过程:计算线程数了解了线程池的实现原理和框架后,我们就可以练习和优化线程池的设置了。我们知道环境是多变的,实际上不可能设置一个绝对准确的线程数,但是我们可以通过一些实际的运行因素计算出合理的线程数,避免线程池设置不合理导致的性能问题。问题。我们来看看具体的计算方法。一般来说,多线程执行的任务类型可以分为CPU密集型和I/O密集型。根据不同的任务类型,我们有不同的计算线程数的方法。CPU密集型任务。这些任务主要消耗CPU资源。线程数可以设置为N(CPU核数)+1,比CPU核数多一个线程是为了防止线程偶尔出现pagefault中断,或者其他原因造成的任务挂起的影响。一旦任务挂起,CPU就空闲了,此时多出一个线程可以充分利用CPU的空闲时间。下面我们通过一个例子来验证这种方法的可行性。可以通过观察CPU密集型任务在不同线程数下的性能得到结果。可以点击Github下载并在本地运行测试:publicclassCPUTypeTestimplementsRunnable{//整体执行时间,包括在队列中的等待时间ListwholeTimeList;//真实执行时间ListrunTimeList;privatelonginitStartTime=0;/***构造函数*@paramrunTimeList*@paramwholeTimeList*/publicCPUTypeTest(ListrunTimeList,ListwholeTimeList){initStartTime=System.currentTimeMillis();this.runTimeList=runTimeList;this.wholeTimeList=wholeTimeList;}/***判断素数*@paramnumber*@return*/publicbooleanisPrime(finalintnumber){if(number<=1)returnfalse;for(inti=2;i<=Math.sqrt(number);i++){if(number%i==0)returnfalse;}returntrue;}/***素数的计算*@paramnumber*@return*/publicintcountPrimes(finalintlower,finalintupper){inttotal=0;for(inti=lower;i<=upper;i++){if(isPrime(i))total++;}returntotal;}publicvoidrun(){longstart=System.currentTimeMillis();countPrimes(1,1000000);longend=System.currentTimeMillis();longwholeTime=end-initStartTime;longrunTime=结束-开始;wholeTimeList.add(wholeTime);runTimeList.add(runTime);System.out.println("单线程耗时:"+(end-start));}}测试代码运行在4核inteli5CPU机器上时间变化如下:总结:当线程数过少时,会同时有大量请求阻塞在线程队列中等待执行线程,此时CPU没有得到充分利用这次;当线程数过多时,同时创建的执行线程会争夺CPU资源,会导致大量的上下文切换,增加线程的执行时间,影响整体执行效率。测试可以看出,4~6个线程是最合适的。当应用I/O密集型任务时,系统会花费大部分时间处理I/O交互,线程在I/O处理时间段内不会占用CPU进行处理。这时候,CPU就可以交给其他线程使用了。因此,在I/O密集型任务的应用中,我们可以配置更多的线程,具体计算方式为2N。这里我们还是用一个例子来验证这个公式是否可以标准化:publicclassIOTypeTestimplementsRunnable{//整体执行时间,包括在队列中的等待时间VectorwholeTimeList;//真正执行时间VectorrunTimeList;privatelonginitStartTime=0;/***构造函数*@paramrunTimeList*@paramwholeTimeList*/publicIOTypeTest(VectorrunTimeList,VectorwholeTimeList){initStartTime=System.currentTimeMillis();this.runTimeList=runTimeList;this.wholeTimeList=wholeTimeList;}/***IO操作*@paramnumber*@return*@throwsIOException*/publicvoidreadAndWrite()throwsIOException{FilesourceFile=newFile("D:/test.txt");//创建输入流BufferedReaderinput=newBufferedReader(newFileReader(sourceFile)));//读取源文件并写入新文件Stringline=null;while((line=input.readLine())!=null){//System.out.println(line);}//关闭输入输出流input.close();}publicvoidrun(){longstart=System.currentTimeMillis();try{readAndWrite();}catch(IOExceptione){//TODOAuto-generatedcatchblocke.printStackTrace();}longend=System.currentTimeMillis();longwholeTime=end-initStartTime;longrunTime=end-start;wholeTimeList.add(wholeTime);runTimeList.add(runTime);System.out.println("单线程耗时:"+(end-start));}}备注:由于测试代码读取一个2MB的文件涉及到大量的内存,所以在运行之前,我们需要调整JVM的堆内存空间:-Xms4g-Xmx4g,避免频繁的FullGC,影响测试结果通过测试结果,我们可以看到即每个线程花费的时间。当线程数为8时,线程的平均执行时间是最优的,这个线程数和我们计算公式得到的结果差不多。看完上面两种情况下的线程计算方式,你可能还想说,在普通的应用场景中,我们往往不会遇到这两种极端情况,所以当我们遇到一些常规的业务操作时,比如通过一个线程的pool实现了定时给用户推送消息的业务,那么线程池的个数应该怎么设置呢?此时,我们可以参考下面的公式来计算线程数:WT:线程等待时间ST:线程运行时间我们可以通过JDK自带的工具VisualVM查看WT/ST比值。以下示例基于运行纯CPU操作。我们可以看到:WT(线程等待时间)=36788ms【线程总运行时间】-36788ms【ST(线程时间运行时间)】=0线程数=N(CPU核心数)*(1+0【WT(线程等待时间)time)]/36788ms[ST(threadtime运行时间)])=N(CPU核心数)这和我们之前CPU密集型计算公式N+1得到的结果类似。整体来说,我们可以根据自己的业务场景,从“N+1”和“2N”这两个公式中选择一个合适的,计算出一个大概的线程数,然后通过实际压测逐步增加线程数.增加线程数”和“减少线程数”两个方向调整,然后观察整体处理时间的变化,最后确定具体的线程数。总结这篇文章,我们主要学习了实现原理线程池,Java线程的创建和消耗会给系统带来性能开销,因此Java提供了线程池来重用线程,提高程序的并发效率。Java是通过1:1的线程模型结合用户线程和内核线程来实现的。Java在用户态设置线程调度和管理,并提供了一套Executor框架帮助开发者提高效率。Executor框架不仅包括线程池管理,还提供了线程工厂、队列和拒绝策略。可以说Executor框架为并发编程提供了完整的架构。在不同的业务场景和不同配置的部署机器上,线程池的线程数设置不同。它的设置不宜过大或过小。应根据具体情况计算出一个近似值,再通过实际性能测试计算出合理的线程数。要想提高线程池的处理能力,首先要保证合理的线程数,即最大化CPU处理线程数。在此前提下,我们增加线程池队列,通过队列缓存以后不能处理的线程。在设置缓存队列时,我们应该尽量使用有界队列,防止队列过大导致的内存溢出问题