Java线程池_java

2014拍摄于四川羌族藏族自治区郎木寺下早课。


Java线程池_java_02

王皓的GitHub:https://github.com/TenaciousDWang


前面已经说了关于线程的东西,接下来说一下线程池。


前面我们知道多线程能够让我们充分的利用计算机资源,前面我们在使用线程时,是直接创建一个线程,创建时需要在内存划出线程的私有空间,比如程序计数器,本地方法栈等,当线程使用完毕时,系统需要回收这些空间,创建与回收都是需要消耗系统资源的,如果我们频繁的创建与销毁那么将会造成系统资源的大量浪费。


如果我们的线程能够复用,执行完任务后不用销毁,就可以继续执行下一个任务,这样就避免了重复创建销毁,提高了效率,节约了系统资源。这时候,我们就需要使用到线程池。


线程池的作用包括。


1、利用线程池管理和复用线程,控制最大并发。


2、实现任务线程队列缓存和拒绝机制。


3、实现计划和定时,周期执行。


4、线程池可以将不同功能的线程进行隔离,避免互相影响。


接下来我们看一下这个类,java.uitl.concurrent.ThreadPoolExecutor,在JUC包中ThreadPoolExecutor就是线程池最核心的类。


首先我们先看一下ThreadPoolExecutor的几个核心参数。


第一个corePoolSize。


Java线程池_java_03


常驻核心线程数,最小值为0,等于0时没有任何请求进入则销毁线程池,大于0时即使执行完所有任务也不会被销毁,注意如果这个值设置过小,则容易引起线程池频繁销毁和创建,过大则浪费资源,所以需要根据实际情况来设置。


第二个maximumPoolSize。


Java线程池_java_04


线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程,必须大于等于1,队列满了时,会创建新线程最大值为上限,需要参考第五个参数,缓存于队列中,如果maximumPoolSize=corePoolSize则为固定大小线程池。


第三个keepAliveTime。



线程空闲时间,表示线程没有任务执行时最多保持多久时间会被销毁,直到剩下corePoolSize个数的线程为止,避免浪费。当线程数大于corePoolSize时才会起作用,也可以设置allowCoreThreadTimeOut变量设置为true时,核心线程超时也会被收回。


第四个TimeUnit,用来表示keepAliveTime的时间单位,通常为TimeUnit.SECONDS。


TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒


第五个workQueue。


Java线程池_java_05


缓存队列,当请求的线程数大于corePoolSize时,多出的线程进入BlockingQueue中。


第6个threadFactory。


Java线程池_java_06


线程工厂,生产一组相同任务的线程,线程池的命名可以通过给factory增加组名前缀来实现,在查看日志时更清晰。


Java线程池_java_07


可以参照自带的默认线程工厂实现写一个自己的。


第七个handler。


Java线程池_java_08


表示当拒绝处理任务时的策略,在当缓存队列达到上限时,且活动线程数大于maximumPoolSize的时候。是一种简单的限流保护。有以下四种取值。


ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 


ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

 

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。


ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。 


举一个例子简单实现自定义的handler:


Java线程池_java_09



友好的拒绝策略,参考自《码出高效》:


1、保存到数据库进行削峰填谷,空闲时间提取执行。


2、转向某个提示页面。


3、打印日志。


假如有一个工厂,工厂里面有10(corePoolSize=10)个工人(线程),每个工人同时只能做一件任务(执行线程)。


只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做。


当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待(阻塞队列缓存)。


如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招5个临时工人进来(maximumPoolSize=10+5)后就将任务也分配给这5个临时工人做。


如果说着15个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了(handler处理策略)。


当这15个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉5个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的(消耗资源的)。


线程池状态。


Java线程池_java_10


RUNNING:当创建线程池后,初始时,处于RUNNING状态的线程池能够接受新任务,以及对新添加的任务进行处理。


SHUTDOWN:如果调用了shutdown()方法,处于SHUTDOWN状态的线程池不可以接受新任务,但是可以对已添加的任务进行处理。


STOP:如果调用了shutdownNow()方法,处于STOP状态的线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

TIDYING:当所有的任务已终止,任务数量为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行函数terminated()。


Java线程池_java_11


terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。


TERMINATED:线程池彻底终止的状态。当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。


任务提交流程分析。


在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,通过submit也可以提交任务,其中ExecutorService.submit()可以获取该任务执行的Future。实际上submit方法里面最终调用的还是execute()方法,所以我们这里以execute()方法为例来分析流程。


Java线程池_java_12


执行流程翻译如下:


1.如果线程池当前线程数小于corePoolSize,则调用addWorker创建新线程执行任务,成功返回true,失败执行步骤2。


2.如果线程池处于RUNNING状态,则尝试加入阻塞队列,如果加入阻塞队列成功,则尝试进行Double Check,如果加入失败,则执行步骤3。


3.如果线程池不是RUNNING状态或者加入阻塞队列失败,则尝试创建新线程直到maxPoolSize,如果失败,则调用reject()方法运行相应的拒绝策略。


在步骤2中如果加入阻塞队列成功了,则会进行一个Double Check的过程。Double Check过程的主要目的是判断加入到阻塞队里中的线程是否可以被执行。如果线程池不是RUNNING状态,则调用remove()方法从阻塞队列中删除该任务,然后调用reject()方法处理任务。否则需要确保还有线程执行。


private boolean addWorker(Runnable firstTask, boolean core){}


这个方法就不展开了,他会根据当前线程池状态,检查是否可以添加新的任务线程,如果可以则创建并启动任务,如果一切正常返回true,如果返回false的可能性如下:


1.线程池没有处于RUNNING状态,2.线程工厂创建新的任务线程失败。


接下来我们举一代码示例来看一下线程池的使用。首先创建一个任务。


Java线程池_java_13


然后我们创建一个test类,创建一个线程池,循环创建15个任务提交至线程池运行。


Java线程池_java_14


运行main方法。


Java线程池_java_15


当线程数超过5时,会将后面的线程放入队列中缓存,如果继续增加,则会创建新的线程直到maximumPoolSize为10时,也就是最多能存在maximumPoolSize加上缓存队列的线程,如果超出就会触发handler的拒绝策略。


我们在说ThreadPoolExecutor时,队列,线程工厂,拒绝服务策略都必须有实例对象,在实际编程中,却很少见过,在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类(java.util.concurrent.Executors)中提供的几个静态方法来创建线程池(带参ThreadFactory的原理一致不再赘述):


Java线程池_java_16


第一个newFixedThreadPool(int nThreads):输入固定的线程数,核心线程数,最大线程数一样,不存在空闲线程,keppAliveTime为0,需要注意这里new LinkedBlockingQueue<Runnable>()没有指定长度,最大值为Integer.MAX_VALUE非常危险会产生OOM,容易耗尽资源。


Java线程池_java_17


第二种newSingleThreadExecutor():创建一个单线程的线程池,相当于单线程串行执行所有任务,保证顺序,注意这里的new LinkedBlockingQueue<Runnable>()也没有定义长度。



第三种ScheduledThreadPoolExecutor(int corePoolSize)最大线程数直接拉到Integer.MAX_VALUE,存在OOM风险,我们看到他的队列,支持定时和周期性任务。


Java线程池_java_18


第四种newCachedThreadPool(),最大线程数拉到Integer.MAX_VALUE,也存在OOM,空闲线程60秒会回收。


Java线程池_java_19


第五种newWorkStealingPool(),JDK8引入,通过创建多个队列减少竞争,适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executor类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。


最后引用《码出高效》的总结。


1、合理设置参数,根据实际工作场景配置线程数。


2、线程资源必须通过线程池提供,不允许私自显式创建线程。


3、创建线程与线程池指定有意义的线程名称,方便查询BUG。


4、线程池不允许使用Executors,而要通过ThreadPoolExcutor创建,明确线程池参数与运行规则,避免资源耗尽。