线程池核心参数有什么,拒绝策略有什么

线程池的核心参数主要定义了线程池如何管理线程、任务以及在资源不足时的行为。在Java的java.util.concurrent.ThreadPoolExecutor中,这些核心参数体现在其构造函数中:

  1. corePoolSize (核心线程数):

    • 线程池中保持存活的核心线程数量,即使它们处于空闲状态,也不会被销毁(除非设置了allowCoreThreadTimeOut)。
    • 当提交一个新任务到线程池时,如果当前运行的线程数少于corePoolSize,即使其他工作线程处于空闲状态,线程池也会创建一个新线程来处理该任务。
  2. maximumPoolSize (最大线程数):

    • 线程池允许创建的最大线程数量。
    • 当工作队列已满,并且当前运行的线程数小于maximumPoolSize时,线程池会创建新的非核心线程来处理任务。
    • 如果corePoolSize等于maximumPoolSize,则创建的是一个固定大小的线程池。
  3. keepAliveTime (线程空闲存活时间):

    • 当线程池中的线程数量大于corePoolSize时,如果一个非核心线程的空闲时间超过keepAliveTime,这个线程就会被终止,以回收资源。
    • 这个参数只有当线程数大于corePoolSize时才有效。
    • 如果设置了allowCoreThreadTimeOut(true),那么核心线程在空闲时间超过keepAliveTime后也会被终止。
  4. unit (时间单位):

    • keepAliveTime参数的时间单位,例如TimeUnit.SECONDS, TimeUnit.MILLISECONDS等。
  5. workQueue (工作队列 / 任务队列):

    • 用于保存在核心线程都在忙,且无法立即处理的新提交任务的队列。它是一个BlockingQueue接口的实现。
    • 常见的队列类型有:
      • ArrayBlockingQueue:基于数组的有界阻塞队列,按FIFO(先进先出)原则排序。
      • LinkedBlockingQueue:基于链表的阻塞队列,按FIFO排序。容量可以是有界的,也可以是无界的(默认情况下容量为Integer.MAX_VALUE,近似无界)。使用无界队列时,maximumPoolSize参数可能不会生效,因为任务会一直入队。
      • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。通常用于需要直接将任务从生产者交给消费者的场景,或者配合一个非常大的maximumPoolSize使用(如CachedThreadPool)。
      • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
    • 队列的选择对线程池的行为有很大影响。
  6. threadFactory (线程工厂):

    • 用于创建新线程的工厂。可以自定义线程的名称、是否为守护线程、优先级等。
    • 默认情况下,线程池使用Executors.defaultThreadFactory(),创建的线程具有相同的优先级和非守护状态,线程名为"pool-N-thread-M"。
  7. handler (拒绝策略 / 饱和策略 - RejectedExecutionHandler):

    • 当工作队列已满,并且线程池中的线程数也达到了maximumPoolSize时,线程池会采取指定的策略来处理无法接收的新任务。

线程池的拒绝策略 (RejectedExecutionHandler) 主要有以下几种Java中预定义的实现:

  1. AbortPolicy (中止策略):

    • 这是ThreadPoolExecutor默认的拒绝策略。
    • 当新任务无法提交时,会抛出RejectedExecutionException运行时异常,调用者可以捕获这个异常并进行处理。
    • 这种策略会直接中断任务的提交过程。
  2. CallerRunsPolicy (调用者运行策略):

    • 当新任务无法提交时,该任务不会被丢弃,也不会抛出异常。而是由提交任务的那个线程(调用execute方法的线程)自己来执行这个任务。
    • 这种策略提供了一种简单的反馈控制机制,可以减慢新任务的提交速度,因为提交者自己忙于执行任务时,就无法快速提交更多任务。
  3. DiscardPolicy (丢弃策略):

    • 当新任务无法提交时,会默默地丢弃该任务,不抛出异常,也不做任何其他处理。
    • 如果允许任务丢失,这是一种简单的策略。
  4. DiscardOldestPolicy (丢弃最旧策略):

    • 当新任务无法提交时,会丢弃工作队列队头(即等待时间最长)的任务,然后尝试重新提交当前这个新任务。
    • 如果工作队列是优先级队列,那么丢弃的是优先级最低的任务。
    • 这种策略也可能会丢失任务。

除了这四种预定义的策略,开发者还可以通过实现RejectedExecutionHandler接口来定义自己的拒绝策略,以满足特定的业务需求。例如,可以将任务持久化到磁盘,或者记录日志后丢弃,或者等待一段时间后重试等。

拓展延申:

  1. 线程池状态: ThreadPoolExecutor内部维护了线程池的状态,主要有:

    • RUNNING: 能够接受新任务,并处理已添加的任务。
    • SHUTDOWN: 不接受新任务,但会处理已添加的任务。调用shutdown()方法会进入此状态。
    • STOP: 不接受新任务,不处理已添加的任务,并中断正在处理的任务。调用shutdownNow()方法会进入此状态。
    • TIDYING: 所有任务都已终止,workerCount为0,线程池会进入该状态并准备调用terminated()钩子方法。
    • TERMINATED: terminated()方法已完成。
  2. 线程池的预热和动态调整:

    • 预热:可以在线程池启动时,通过调用prestartAllCoreThreads()prestartCoreThread()来预先创建核心线程,避免在接收到初始任务时才开始创建线程的延迟。
    • 动态调整:ThreadPoolExecutor提供了setCorePoolSize(), setMaximumPoolSize(), setKeepAliveTime()等方法,允许在运行时动态调整线程池的参数。这可以用于根据系统负载情况自适应地调整线程池配置,但需要谨慎操作,避免不当调整导致性能问题或资源浪费。
  3. 监控线程池: ThreadPoolExecutor提供了很多方法用于监控其状态,如:

    • getPoolSize(): 当前线程池中的线程数量。
    • getActiveCount(): 当前正在执行任务的线程数量。
    • getCompletedTaskCount(): 已完成的任务数量。
    • getTaskCount(): 曾计划执行的任务总数(包括已完成、正在执行和队列中等待的)。
    • getQueue().size(): 当前工作队列中的任务数量。 这些信息对于监控线程池的健康状况、诊断问题以及调优参数非常有价值。许多APM工具也会集成对线程池的监控。
  4. 合理配置线程池大小: 这是一个常见且重要的问题,没有万能公式,需要根据任务类型(CPU密集型 vs. I/O密集型)、任务处理时间、系统资源(CPU核心数、内存)、期望的吞吐量和响应时间等因素综合考虑。

    • CPU密集型任务:线程数通常建议设置为CPU核心数或核心数+1,以减少上下文切换。
    • I/O密集型任务:由于线程在等待I/O时会阻塞,可以配置更多的线程,例如 CPU核心数 * (1 + 平均等待时间 / 平均计算时间)。一个经验公式是 CPU核心数 / (1 - 阻塞系数),阻塞系数在0到1之间。 实际配置往往需要通过压力测试和性能监控来调整和优化。