线程池全面解析,一篇就够了!

为何使用线程池

通常状况有下面三个缘由java

  • 建立/销毁线程伴随着系统开销,过于频繁的建立/销毁线程,会很大程度上影响处理效率,应用可以更加充分合理地协调利用CPU、内存、网络、I/O等系统资源;

为何会影响处理效率呢?
答:线程的建立须要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间; 在线程销毁时须要回收这些系统资源.频繁地建立和销毁线程会浪费大量的系统资源,增长并发编程风险.android

  • 线程并发数量过多,抢占系统资源从而致使阻塞,利用线程池管理并复用线程,控制最大并发数;
  • 使用线程池能够对线程进行一些简单的管理,实现任务线程队列缓存策略和拒绝机制,实现某些与时间相关的功能,例如定时执行、周期执行等,另外还能够隔离线程环境;

线程池ThreadPoolExecutor

Android中线程池来自于Java,那么研究Android线程池其实也能够说是研究Java中的线程池git

在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,学习Java中的线程池,就能够直接学习他了面试

对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置,既然这些参数这么重要,就来看看构造函数的各个参数吧数据库

ThreadPoolExecutor的重要参数

  • corePoolSize:核心线程数核心线程会一直存活,即便没有任务须要执行 当线程数小于核心线程数时,即便有线程空闲,线程池也会优先建立新线程处理 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
  • workQueue:任务队列(阻塞队列)当核心线程数达到最大时,新任务会放在队列中排队等待执行,该线程池中的任务队列:维护着等待执行的Runnable对象

当全部的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,若是队列满了,则新建非核心线程执行任务编程

经常使用的workQueue类型:缓存

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,若是全部线程都在工做怎么办?那就新建一个线程来处理这个任务!因此为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize通常指定成Integer.MAX_VALUE,即无限大安全

  2. LinkedBlockingQueue:这个队列接收到任务的时候,若是当前线程数小于核心线程数,则新建线程(核心线程)处理任务;若是当前线程数等于核心线程数,则进入队列等待。因为这个队列没有最大值限制,即全部超过核心线程数的任务都将被添加到队列中,这也就致使了maximumPoolSize的设定失效,由于总线程数永远不会超过corePoolSizebash

  3. ArrayBlockingQueue:能够限定队列的长度,接收到任务的时候,若是没有达到corePoolSize的值,则新建线程(核心线程)执行任务,若是达到了,则入队等候,若是队列已满,则新建线程(非核心线程)执行任务,又若是总线程数到了maximumPoolSize,而且队列也满了,则发生错误网络

  4. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

  • maxPoolSize:最大线程数

    • 当线程数>=corePoolSize,且任务队列已满时。线程池会建立新线程来处理任务
    • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
  • keepAliveTime:线程空闲时间 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize 若是allowCoreThreadTimeout=true,则会直到线程数量=0 方法allowCoreThreadTimeout()能够设置容许核心线程超时退出

  • TimeUnit :keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:

    • NANOSECONDS : 1微毫秒 = 1微秒 / 1000
    • MICROSECONDS : 1微秒 = 1毫秒 / 1000
    • MILLISECONDS : 1毫秒 = 1秒 /1000
    • SECONDS : 秒
    • MINUTES : 分
    • HOURS : 小时
    • DAYS : 天
  • ThreadFactory 建立线程的方式,是一个接口,new他的时候须要实现他的Thread

  • rejectedExecutionHandler:任务拒绝处理器

    • 两种状况会拒绝处理任务:
      • 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
      • 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。若是在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务 线程池会调用rejectedExecutionHandler来处理这个任务。若是没有设置默认是AbortPolicy,会抛出异常

ThreadPoolExecutor类有几个内部实现类来处理这类状况:

  • AbortPolicy 丢弃任务,抛运行时异常
  • CallerRunsPolicy 执行任务
  • DiscardPolicy 忽视,什么都不会发生
  • DiscardOldestPolicy 从队列中踢出最早进入队列(最后一个执行)的任务
  • 实现RejectedExecutionHandler接口,可自定义处理器

ThreadPoolExecutor执行顺序

线程池按如下行为执行任务

面试时可能常常会被问到的一个问题

  • 当线程数小于核心线程数时,建立核心线程执行任务。
  • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  • 当线程数大于等于核心线程数,且任务队列已满
    • 若线程数小于最大线程数,建立线程执行任务
    • 若线程数等于最大线程数,抛出异常,拒绝任务,异常策略是上面介绍的那些类型

线程池中任务执行

通常状况下有两种方式能够向线程池中提交任务

  • execute 调用execute()方法提交任务到线程池中进行执行,这个方法是没有返回值的,比较经常使用,是ThreadPoolExecutor本身实现的方法;这种方式没法判断任务被线程池执行的状况,好比是否成功;

  • submit 调用submit方法会有返回,完整的方法描述是:public Future<?> submit(Runnable task),其中泛型T是咱们回调的结果类型,若是咱们但愿监放任务执行的结果,可使用submit方法提交任务,这个方法实际上是ThreadPoolExecutor的父类AbstractExecutorService的方法;
    返回的future可经过get()获取返回值,get()会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后当即返回,这时候可能任务没有执行完.

默认的经常使用线程池

通常咱们可能不须要本身去根据实际须要进行参数设定对应的ThreadPoolExecutor来实现线程池,大部分状况下,咱们使用java并发包中提供的Executors的类调用它的静态方法获取须要的线程池类型

  • newFixedThreadPool 建立固定线程数的线程池由于最大线程数和核心线程数相等,而且是无界队列,可控制线程最大并发数(同时执行的线程数),超出的线程会在队列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
复制代码
  • newSingleThreadExecutor 单任务队列的线程池,最大线程数和核心线程数都是1,无界队列,全部的任务都按照顺序进行执行;
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
复制代码
  • ScheduledThreadPool 支持定时周期性执行任务的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
复制代码
  • CachedThreadPool 线程数无限制 有空闲线程则复用空闲线程,若无空闲线程则新建线程 必定程序减小频繁建立/销毁线程,减小系统开销
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
复制代码

线程池的关闭

可经过调用线程池的shutdown或shutdownNow方法来关闭线程池.

它们的原理是遍历线程池中的工做线程,而后逐个调用线程的interrupt方法来中断线程,因此没法响应中断的任务可能永远没法终止.

可是它们存在必定的区别

  • shutdownNow首先将线程池的状态设置成STOP,而后尝试中止全部的正在执行或暂停任务的线程,并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态,而后中断全部没有正在执行任务的线程.

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true.

当全部的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true.

至于应该调用哪种方法,应该由提交到线程池的任务的特性决定,一般调用shutdown方法来关闭线程池,若任务不必定要执行完,则能够调用shutdownNow方法.

线程池的状态

  • running状态:当线程池建立后,初始为 running 状态
  • shutdown状态:调用 shutdown 方法后,处在shutdown 状态,此时再也不接受新的任务,等待已有的任务执行完毕
  • stop状态:调用 shutdownnow 方法后,进入 stop 状态,再也不接受新的任务,而且会尝试终止正在执行的任务。
  • terminated状态:当处于 shotdown 或 stop 状态,而且全部工做线程已经销毁,任务缓存队列已清空,线程池被设为 terminated 状态。

线程池的配置选择

要想合理地配置线程池,就必须首先分析任务特性,可从如下几个角度来分析

  • 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
  • 任务的优先级:高、中和低
  • 任务的执行时间:长、中和短
  • 任务的依赖性:是否依赖其余系统资源,如数据库链接。

针对不一样性质的任务

使用不一样规模的线程池进行处理

  • CPU密集型任务 应配置尽量小的线程,配置 N(CPU)+1或者 N(CPU) * 2
  • I/O密集型任务 业务读取较多,线程并非一直在执行任务,则应配置尽量多的线程 N(CPU)/1 - 阻塞系数(0.8~0.9)
  • 混合型的任务 若是能够拆分,将其拆分红一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量.若是这两个任务执行时间相差太大,则不必进行分解.

经过Runtime.getRuntime().availableProcessors()方法得到当前设备的CPU个数

针对优先级不一样的任务

优先级不一样的任务可使用PriorityBlockingQueue处理.它可让优先级高 的任务先执行. 若是一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行,这一点须要特别注意

针对执行时间不一样的任务

执行时间不一样的任务能够交给不一样规模的线程池来处理,或者可使用优先级队列,让执行时间短的任务先执行.

建议使用有界队列 有界队列能增长系统的稳定性和预警能力,若是咱们设置成无界队列,那么线程池的队列就会愈来愈多,有可能会撑满内存,致使整个系统不可用,而不仅是后台任务出现问题.固然这些对于java后台的开发比较关键,对于android的开发相对比较少一些;

线程池的监控

若是在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,能够根据线程池的使用情况快速定位问题.可经过线程池提供的参数进行监控,在监控线程池的时候可使用如下属性:

taskCount:线程池须要执行的任务数量
completedTaskCount:线程池在运行过程当中已完成的任务数量,小于或等于taskCount。
largestPoolSize:线程池里曾经建立过的最大线程数量.经过这个数据能够知道线程池是否曾经满过.如该数值等于线程池的最大大小,则表示线程池曾经满过.
getPoolSize:线程池的线程数量.若是线程池不销毁的话,线程池里的线程不会自动销毁,因此这个大小只增不减.
getActiveCount:获取活动的线程数.
复制代码

经过扩展线程池进行监控.能够经过继承线程池来自定义线程池,重写线程池的 beforeExecute、afterExecute和terminated方法,也能够在任务执行前、执行后和线程池关闭前执行一些代码来进行监控.例如,监控任务的平均执行时间、最大执行时间和最小执行时间等.

往期文章推荐
那些年你曾经工做过的奇葩公司能有多奇葩!
Kotlin扩展和对应的java代码解析
git操做高级命令
了解一下计算机网络层吧
计算机网络基础
带你深刻了解运输层
看看网络安全的一些知识吧!
是时候了解一下应用层的知识了一块儿来吧!
如何使用命令行卸载手机预知APP

长按二维码关注公众号,接收新的消息推送,值得期待哟!感谢您的支持!

技术干货店
相关文章
相关标签/搜索