线程池
设计架构以及主要类的职责
首先,我们来看看线程池架构的基本组成
- Executor 顶级接口,定义了线程池的能力,只有一个exetcute方法,即开发者只需要提交任务,不用关心其他细节
- ExecutorService 继承了Executor的扩展接口,扩充了任务提交获取结果的方法submit,主动关闭线程池的方法shutdown等
- AbsExecutorService 线程池的抽象父类,是ExecutorService 接口是具体实现类
- ThreadPoolExecutor 线程池的实现类,提供了线程池生命周期管理、线程创建和管理、任务调度等功能
ThreadPoolExecutor构造参数的含义
我们通常使用ThreadPoolExecutor这个类实现线程池,它的构造函数如下图,通过配置不同的参数,可以构建出行为差异巨大的线程池
- corePoolSize 核心线程数,可以理解为需要长期预留的线程数目,除非设置了allowCoreThreadTimeout
- maximumPoolSize 最大线程数,允许创建的最大线程数目,
- keepAliveTime 非核心线程等待超时时间
- unit 时间单位
- workQueue 任务的缓存队列,必须是阻塞队列
- threadFactory 控制线程创建的工作
- handler 拒绝任务策略
其中threadFactory、handler不是必须的,线程池有默认实现
线程池的工作流程
当开发者调用execute()提交任务后,执行流程如下
- 如果 线程数<核心线程数,创建新的线程,并执行任务
- 如果当前线程数>核心线程数,尝试把任务加入任务队列。
- 如果任务队列已满,入队失败,并且 当前线程数< 最大线程数,创建新线程执行任务
- 如果 线程数>最大线程数,执行拒绝策略
线程池生命周期
线程池有自己的生命周期,但不是用户显示设置的,而是伴随着任务的执行,由线程池内部维护的。线程池用一个Int变量ctl维护两个值:runState(线程池生命周期状态)和workerCount(当前线程数 ),高3位表示生命周期,低29位表示当前线程数。同时提供了对应方法来获取当前状态和有效线程数数量,这是一个高效优化,因为线程池内部有大量使用这2个值的地方,用一个变量来存储,可以避免在做对用决策时,出现不一致的情况,也省去加锁带来的性能开销
线程池生命周期如下
- RUNNING 运行,接受新任务,也可以处理任务队列中的任务
- SHUTDOWN 中断,不接受新任务,但是可以处理任务队列中的任务
- STOP 停止,不接受新任务,也不处理队列中的任务,并且会中断执行任务的线程
- TIDYING 所有线程都已经中断、清理,有效线程数等于0
- TERMINATED 终止,回调terinal后进入该状态,表示线程池已死
生命周期转换如下图
默认提供的几种线程池实现
Executors这个类提供了几种线程池的默认实现,其实就是配置不同的构造参数ThreadPoolExecutor,从而创建出行为差异巨大,适应不同场景的线程池
- newFixedThreadPool() 固定线程数量的线程池,没有达到线程数上限时,会创建新的线程。否则,会缓存任务排队执行。使用一个有界队列LinkedBlockingQueue缓存任务
- newSingleThreadPool()线程数固定为1的线程池,使用一个有界阻塞队列LinkedBlockingQueue缓存任务,所有任务排队串行执行
- newCacheThreadPool 缓存线程并定时回收的线程池,使用SynchronousQueue同步队列,这个队列容量为0,任务入队必须对应着一个出队操作,否则就会阻塞,反之亦然。提交的任务都会马上执行,线程空闲等待超时会回收,直到线程数收缩为0,长时间运行,不会消耗什么资源
- newScheduleThreadPool() 可以定时或者周期性执行任务的线程池
源码分析
任务调度
我们通常使用execut方法提交任务,下图的源码非常清楚的提现了上面所述的任务调度流程
工作线程
线程池的工作线程被抽象成worker类,worker实现了Runnable接口,持有一个线程thread和一个初始任务firstTask,如果firstTask不为null,这个任务会被马上执行。如果为null,则线程会从任务队列中取出任务执行。
worker继承了AbstractQueuedSynchronizer,也就是常说的AQS,提供了lock和unlock等一系列方法表示当前线程是否闲置,在没有任务的时候,闲置的线程池会被回收,以保证线程数量等于核心线程数
创建工作线程
从上图可以看到,创建工作线程的逻辑是addWorker,首先根据core值判断线程数是否超过限制,创建工作线程或者返回失败。如果创建成功,把线程引用添加进集合,调用worker内部的thread.start()启动线程,最终调用到worker.runWork()执行任务
因为线程池本身就是一个高并发环境,主要方法的里面都会有各种同步、生命周期状态的判断
任务获取和执行
worker的runWork()负责任务的获取和执行。线程以不断轮训的方式从任务队列中取出任务(firstTask不为空时立即执行),
从任务队列中获取任务是在getTask()方法中实现的,抛开各种同步和生命周期的判断,核心逻辑其实就一句
队列为空时,如果allowCoreThreadTimeout 或者 wc>corePoolSize,调用线程进入超时等待(WAIT_TIMEOUT)状态,超时自动退出,否则线程进入等待(WAIT)状态,直到队列不为空时才会返回
线程清理
当线程正常退出轮训,说明队列中没有任务可执行了并且当前线程数超过了核心线程(或者设置了allowCoreThreadTimeOut=true),就要清理线程,使线程始终保持在设定的值,当然线程异常退出(执行过程中抛了异常或者被中断)也是要需要清理的
清理线程过程中,会尝试改变线程池状态为TERMINATED,为啥?因为线程池提供了shutdown()方法,不再需要的时候开发者需要手动关闭线程池。
这个过程中,还会调用interruptIdleWorkers()中断闲置的线程,就是遍历线程集合,根据线程是否加锁判断线程是否闲置,如果是直接调用Thread.interrupt进行中断。
实践中常见问题
- 避免任务堆积,newFixedThreadPool创建固定数目的线程,使用的任务队列是无界的,如果任务的处理速度跟不上入队的速度,就会导致任务大量堆积,占用大量系统内存,严重时导致OOM
- 注意线程泄漏,如果线程数目不断的增长,可能是因为任务逻辑出现问题,导致线程迟迟不能被释放
- 使用ThreadLocal导致的内存泄漏,ThreadLocal以自己的弱引用对象作为key,使用Thread中的ThreadLocalMap存储数据,ThreadLocal不再使用后,弱引用对象会被GC回收,对应的value永远不会被访问,但如果线程不销毁就会持有ThreadLocalMap进而持有value的强引用,导致内存泄漏。线程池中工作线程的生命周期一般比任务要长,核心线程更是长驻内存。所以在线程池中避免使用ThreadLocal或者使用后及时调用remove释放value的引用
如何根据应用特性选择合理的线程池
- 响应优先:以响应优先的场景,需要任务到来时尽快执行,避免排队。newCacheThreadPool的配置就比较适合
- 吞吐量优先:应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。