Skip to content

线程池介绍

管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。

  • 降低线程频繁创建销毁的成本;
  • 任务到达不需要等待线程创建立刻执行,提高响应速度;
  • 确保线程数量不会过多,提升稳定性。

Executor 框架介绍

还有关键的一点:有助于避免 this 逃逸问题。

this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架结构主要由三大部分组成:

  • 任务,需要传入的对象类实现Runnable /Callable接口
  • 任务的执行:核心接口Executor
  • 异步计算的结果:核心接口Future,调用 submit 提交任务会返回一个FutureTask对象,execute 不会返回这个对象
    • 主线程可以通过FutureTask.get()方法来等待任务执行完成,或者调用FutureTask.cancel()取消任务的执行
    • get 可以设定超时时间,超过了抛异常

ThreadPoolExecutor 类

Executor 框架最核心的线程池实现类,主要有以下几个参数

  • corePoolSize:线程池核心线程数量:会一直存活的线程
    • 初始线程池持有线程为 0,当有任务到来才开始创建
    • CPU 密集型 N+1,IO 密集型 2N,其中 N 是 CPU 核心数
  • maximumPoolSize:线程池最大线程数量:大于等于核心数量,可以允许的最大同时运行数
    • 只有任务队列满了,才会创建新的非核心线程,避免频繁创建销毁
  • keepAliveTime:非核心线程最大空闲存活时间
  • unit:时间单位
  • workQueue:任务队列,存储等待执行任务的队列
    • 无界队列:最大空间Integer.MAX_VALUE
    • 同步队列:没有空间的队列
    • 延迟阻塞队列:按照定义的延迟时间先后排序,由堆实现,最大空间Integer.MAX_VALUE,可以手动继承修改入队逻辑,避免数量过大
    • 有界阻塞队列:固定容量的队列
    • 无界队列由于无限制增长,基本不会触发拒绝策略,也不会创建非核心线程
  • threadFactory:线程工厂,用于创建线程,一般默认
  • handler:拒绝策略,达到最大线程且队列满了后的处理方式
    • 1、默认抛出异常拒绝新任务
    • 2、直接丢弃不处理
    • 3、丢弃队列中最早未处理的任务
    • 4、当前线程直接执行该任务,可能拖慢整体的运行速度,后续任务会阻塞
      • 适合所有任务都不能丢弃的情况
      • 唯一一个不会丢失任务的策略
    • 可以通过消息队列配合抛出异常保证任务不丢失,更加优雅可靠

创建线程池方法

  • ThreadPoolExecutor 手动创建(推荐)
  • Executors 工具类创建:都有 OOM 的风险,不建议使用
    • FixedThreadPool:固定线程数量的线程池。
    • SingleThreadExecutor: 只有一个线程的线程池。
      • 前两者使用无界队列,会 OOM
    • CachedThreadPool:容量无限大的线程池。
      • 使用同步队列,线程过多会 OOM
    • ScheduledThreadPool:通过延迟阻塞队列实现优先级执行。
      • 言辞阻塞队列无界,会 OOM
  • 建议不同类别的业务用不同的线程池,否则可能死锁
    • 试想这样一种极端情况:假如我们线程池的核心线程数为 n,父任务(扣费任务)数量为 n,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 "死锁"
    • 即相互需要对方执行结束,但是一个占据了线程,一个获取不到线程
  • 当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。
  • 注意:线程池和 ThreadLocal共用,可能会导致线程从ThreadLocal获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 ThreadLocal 变量也会被重用,这就导致一个线程可能获取到其他线程的ThreadLocal 值。
    • 不要以为代码中没有显示使用线程池就不存在线程池了,像常用的 Web 服务器 Tomcat 处理任务为了提高并发量,就使用到了线程池,并且使用的是基于原生 Java 线程池改进完善得到的自定义线程池。
  • 线程池尽量不要放耗时任务
    • 如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。
    • 因此,在使用线程池时,我们应该尽量避免将耗时任务提交到线程池中执行。对于一些比较耗时的操作,如网络请求、文件读写等,可以采用 CompletableFuture 等其他异步操作的方式来处理,以避免阻塞线程池中的线程。

常用方法

  • execute/submit:
    • 后者返回 FutureTask 对象,前者不返回
    • 线程执行异常,前者直接销毁该线程后重建,后者封装在对象中由用户处理
  • shutdown/shutdownNoe:前者会将队列中的所有任务执行完毕后关闭线程池,后者立刻关闭
  • isTerminated/isShutdown:前者 shutdown 后立刻返回 true,后者 shutdown 后且完成所有任务后返回 true

正在精进