跳至主要內容

线程池

xw大约 4 分钟JAVAJAVA

线程池存在的意义

Java线程创建需要虚拟机和操作系统完成以下工作:

  • 为线程堆栈分配和初始化大量内存,其中包含至少1MB的栈内存
  • 需要进行系统调用,以便在操作系统中创建和注册本地线程

Java高并发应用频繁创建和销毁线程的操作是非常低效的,为了降低Java线程的创建成本,必须使用到线程池,线程池主要解决了以下两个问题:

  1. 提升性能。线程池能独立负责线程的创建、维护和分配。在执行任务时不需要自己去创建线程,统一将任务交给线程池去调度
  2. 线程管理。每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度

线程池的创建

线程池相关代码架构:

  • Executors为静态工程类提供了4种快捷创建线程池的方法。
  • newFixedThreadPool ,固定线程池
    • newCachedThreadPool, 一个可缓存线程池
    • newSingleThreadExecutor, 一个单线程化的线程池,用唯一的工作线程来执行任务
    • newScheduledThreadPool,一个定长线程池,支持定时/周期性任务执行
  • 核心参数
    • corePoolSize:核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受存keepAliveTime控制,在刚创建线程池时线程不会立即启动,到有任务提交时才开始创建线程并逐步线程数目达到corePoolSize
    • maximumPoolSize:线程池维护线程的最大数量,超过将被阻塞。当核心线程满,且阻塞队列也满时,才会判断当前线程数是否小于最大线程数,才决定是否创建新线程
    • keepAliveTime:非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于corePoolSize。
    • unit:指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.MILLISECONDS
    • workQueue:线程池中的任务队列,常用的是 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue
    • threadFactory:创建新线程时使用的工厂
    • handler: RejectedExecutionHandler是一个接口且只有一个方法,线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认有4种策略AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

任务调度

  1. 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列获取一个空闲线程

  2. 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。

  3. 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。

  4. 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。

  5. 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。

    具体流程图如下所示:

设置线程数

  • IO密集型任务: CPU核心数两倍。

  • CPU密集型任务(计算密集型任务): 线程数等于CPU数

  • 混合型任务: 最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间) * CPU核数

规范

  • 线程池不允许使用 Executors 去创建,直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险。

  • 给不同模块的线程起名称,方便后续排查问题