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

2,327 阅读12分钟

为什么使用线程池

一般情况有下面三个原因

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

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

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

线程池ThreadPoolExecutor

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

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

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

ThreadPoolExecutor的重要参数

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

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

常用的workQueue类型:

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

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

  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

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

技术干货店