掌握线程池先从源码注释出发

189 阅读6分钟

完全掌握线程池是每个Javaer必须要做的事情,线程池的知识不仅仅是面试中的八股 更是让使用者受益匪浅。

本文先从 ThreadPoolExecutor 开头的英文注释出发,搞清楚 Doug Lea (ThreadPoolExecutor 编写者)对使用者想说的话,加粗部分为我自己的补充。

这段代码是Java中ThreadPoolExecutor类的文档注释,主要描述了线程池的工作原理和配置方法。以下是主要内容的中文翻译:

  1. 线程池的作用

    • 线程池使用多个线程来执行提交的任务,通常通过Executors工厂方法进行配置。
    • 线程池解决了两个问题:提高大量异步任务的执行性能(由于减少了每个任务的调用开销),并提供了一种管理和限制资源(包括线程)消耗的方法。
    • 每个ThreadPoolExecutor还维护一些基本统计信息,例如已完成任务的数量。
  2. 推荐使用工厂方法

    • 推荐使用Executors工厂方法中的便捷方法,如newCachedThreadPool(无界线程池,带有自动线程回收)、newFixedThreadPool(固定大小线程池)和newSingleThreadExecutor(单个后台线程),这些方法预配置了最常见的使用场景。
    • 如果需要手动配置和调整线程池,可以参考以下指南。
  3. 核心和最大线程数

    • ThreadPoolExecutor会根据核心线程数(corePoolSize)和最大线程数(maximumPoolSize)自动调整线程池大小。
    • 当提交新任务时,如果运行中的线程数少于核心线程数,则创建新线程处理请求,即使其他工作线程处于空闲状态。
    • 如果运行中的线程数超过核心线程数但小于最大线程数,只有当队列已满时才会创建新线程。
    • 设置相同的核心线程数和最大线程数可以创建固定大小的线程池;将最大线程数设置为一个几乎无限的值(如Integer.MAX_VALUE)可以允许任意数量的并发任务。(这是不推荐的,因为java的平台线程与操作系统线程是1:1的关系,过多的线程会导致cpu过多进行线程切换有上下文切换的开销,同时大量的线程创建也会导致内存紧张出现OOM!
  4. 按需创建线程

    • 默认情况下,即使是核心线程也只在有新任务到达时才创建和启动,但可以通过prestartCoreThreadprestartAllCoreThreads方法动态覆盖这一行为。
    • 如果使用非空队列创建线程池,建议预先启动线程。
  5. 创建新线程

    • 新线程由ThreadFactory创建,默认使用Executors.defaultThreadFactory,它创建的所有线程都在同一个ThreadGroup中,具有相同的优先级和非守护线程状态。
    • 提供不同的ThreadFactory可以更改线程的名称、线程组、优先级、守护状态等。
    • 如果ThreadFactory无法创建线程(返回null),执行器将继续运行,但可能无法执行任何任务。
    • 线程应具备“modifyThread”权限,否则服务可能会降级。
  6. 空闲时间

    • 如果当前线程数超过核心线程数,空闲时间超过keepAliveTime的多余线程将被终止,以减少资源消耗。
    • 此参数可以动态更改,使用Long.MAX_VALUE禁用空闲线程的终止。
    • 默认情况下,空闲策略仅适用于超过核心线程数的线程,但可以通过allowCoreThreadTimeOut方法将其应用于核心线程。
  7. 队列策略

    • 可以使用任何BlockingQueue来传递和保存提交的任务,队列的使用与线程池大小相关:

      • 如果运行中的线程数少于核心线程数,执行器总是优先创建新线程而不是排队。
      • 如果运行中的线程数等于或超过核心线程数,执行器总是优先排队请求而不是创建新线程。
      • 如果请求无法排队且创建新线程会超过最大线程数,则任务将被拒绝。
    • 常见的队列策略包括直接传递(SynchronousQueue)、无界队列(LinkedBlockingQueue)和有界队列(ArrayBlockingQueue)。(SynchronousQueue 也被成为同步队列,该队列没有容量,当执行任务时必须要有可以调度的线程来执行否则会执行拒绝策略,在应用中通常配合CallerRunsPolicy 拒绝策略来满足系统快速响应的线程池, 剩下的Link 和 Array 主要是数据结构的区别,Link 也可以指定容量,这两个主要构造带有有缓冲区域的线程池,这类线程池不追求及时处理主要控制资源消耗

  8. 拒绝策略

    • 当执行器关闭或达到最大线程数和队列容量时,新提交的任务将被拒绝。

    • 四种预定义的拒绝策略:

      • 默认策略AbortPolicy抛出RejectedExecutionException
      • CallerRunsPolicy由调用execute的线程运行任务,减慢新任务的提交速度。
      • DiscardPolicy直接丢弃无法执行的任务。
      • DiscardOldestPolicy丢弃队列中最旧的任务并重试执行。
  9. 钩子方法

    • 提供了可重写的beforeExecuteafterExecute方法,在任务执行前后调用,用于操作执行环境(如重新初始化ThreadLocal、收集统计信息、添加日志条目)。
    • 还可以重写terminated方法,在执行器完全终止后执行特殊处理。
    • 如果钩子或回调方法抛出异常,内部工作线程可能会失败并突然终止。
  10. 队列维护

    • getQueue方法允许访问工作队列,主要用于监控和调试。
    • 提供了removepurge方法,用于在大量已取消的任务中协助存储回收。
  11. 最终化

    • 如果线程池不再被引用且没有剩余线程,它将自动关闭。
    • 为了确保未引用的线程池在用户忘记调用shutdown时也能被回收,应设置适当的空闲时间,使用核心线程数量 为 0或启用allowCoreThreadTimeOut。(这段文字是关于线程池终止情况,如果线程池没有被执行shutdown 以及 核心线程没有结束的情况下 这会导致优雅关闭失败,因为jvm中仍然有用户线程未结束,Spring应用可以使用ThreadPoolTaskExecutor 该包装后的线程池实现了Spring 的 DisposableBean 接口 在spring应用销毁时调用 shutdown

控制流图

flowchart TD
    A[开始] --> B{线程池是否已关闭}
    B -->|是| C[拒绝任务]
    B -->|否| D{当前线程数小于核心线程数}
    D -->|是| E[创建新线程执行任务]
    D -->|否| F{任务加入队列}
    F --> G{队列是否已满}
    G -->|否| H[等待线程处理任务]
    G -->|是| I{当前线程数小于最大线程数}
    I -->|是| J[创建新线程执行任务]
    I -->|否| K[拒绝任务]