Java线程池

1,307 阅读7分钟

线程池

设计架构以及主要类的职责

首先,我们来看看线程池架构的基本组成

  • Executor 顶级接口,定义了线程池的能力,只有一个exetcute方法,即开发者只需要提交任务,不用关心其他细节
  • ExecutorService 继承了Executor的扩展接口,扩充了任务提交获取结果的方法submit,主动关闭线程池的方法shutdown等
  • AbsExecutorService 线程池的抽象父类,是ExecutorService 接口是具体实现类
  • ThreadPoolExecutor 线程池的实现类,提供了线程池生命周期管理、线程创建和管理、任务调度等功能

ThreadPoolExecutor构造参数的含义

我们通常使用ThreadPoolExecutor这个类实现线程池,它的构造函数如下图,通过配置不同的参数,可以构建出行为差异巨大的线程池

  1. corePoolSize 核心线程数,可以理解为需要长期预留的线程数目,除非设置了allowCoreThreadTimeout
  2. maximumPoolSize 最大线程数,允许创建的最大线程数目,
  3. keepAliveTime 非核心线程等待超时时间
  4. unit 时间单位
  5. workQueue 任务的缓存队列,必须是阻塞队列
  6. threadFactory 控制线程创建的工作
  7. handler 拒绝任务策略

其中threadFactory、handler不是必须的,线程池有默认实现

线程池的工作流程

当开发者调用execute()提交任务后,执行流程如下

  1. 如果 线程数<核心线程数,创建新的线程,并执行任务
  2. 如果当前线程数>核心线程数,尝试把任务加入任务队列。
  3. 如果任务队列已满,入队失败,并且 当前线程数< 最大线程数,创建新线程执行任务
  4. 如果 线程数>最大线程数,执行拒绝策略

线程池生命周期

线程池有自己的生命周期,但不是用户显示设置的,而是伴随着任务的执行,由线程池内部维护的。线程池用一个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去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。