线程池的创建

使用Executors创建线程池:

1
2
3
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的线程池

但是不建议使用Executors创建线程池,在阿里巴巴技术规范中写有:

image-20240903160832255

使用Executors创建线程池的本质实际上也是调用了ThreadPoolExecutor创建线程池,但是创建的线程池默认配置不太合理?

TreadPoolExector

ThreadPoolExecutor的构造方法:

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// ...
}

ThreadPoolExecutor的构造函数具有以下七个参数,它们分别是:

  1. corePoolSize(核心线程数):

    它指定了线程池中保持活动状态的核心线程数量。核心线程是一直存活的线程,即使它们处于空闲状态也不会被回收。当有新任务提交时,核心线程会立即执行任务。如果使用的是无界队列,线程池中的线程数永远不会超过核心线程数。

  2. maximumPoolSize(最大线程数):

    它定义了线程池中允许创建的最大线程数量。当工作队列已满且当前线程数小于最大线程数时,线程池会创建新的线程来执行任务。如果使用的是有界队列,当队列已满且线程数达到最大值时,新的任务会触发拒绝策略。

  3. keepAliveTime(线程空闲超时时间):

    它表示非核心线程空闲的最大时间。当线程池中的线程数超过核心线程数,并且空闲时间超过该值时,多余的线程会被终止并从线程池中移除,以减少资源消耗。新任务到达时,如果线程池中的线程数小于核心线程数,可能会重新创建线程。

  4. unit(空闲超时时间的单位):

    用于指定keepAliveTime参数的单位,可以是TimeUnit.SECONDSTimeUnit.MILLISECONDS等。

  5. workQueue(工作队列):

    它定义了用于保存待执行任务的阻塞队列。当任务提交到线程池时,如果线程数小于核心线程数,会创建新线程来执行任务。如果线程数达到核心线程数,而工作队列未满,则将任务放入队列中等待执行。工作队列可以是有界队列(如ArrayBlockingQueue)或无界队列(如LinkedBlockingQueue)。

  6. threadFactory(线程工厂,可选):

    线程工厂指定创建线程的方式,用于创建新线程的工厂对象。线程工厂可以根据需要对线程进行自定义配置,例如设置线程名字、设置线程优先级等。如果未指定,将使用默认的DefaultThreadFactory

  7. handler(拒绝策略,可选):

    它定义了当线程池无法接受新任务时的处理方式。拒绝策略可以是预定义的几种策略,如抛出异常、丢弃任务、阻塞等。也可以根据需要自定义拒绝策略实现RejectedExecutionHandler接口。

我们可以这样类比:

image-20240903161026674
注意:核心线程没有空闲时,超量任务首先放入队列中,队列满了才会开普通线程处理!

如果corePoolSize为0,则要使用SynchronousQueue避免无限阻塞。因为核心线程为0时,任务会先放入队列中,队列放不下了再使用普通线程,如果队列是无限的就会导致一直阻塞

工作队列 BlockQueue

使用用ThreadPoolExecutor需要指定一个BlockingQueue任务等待队列。

BlockingQueue(阻塞队列)定义了一组用于插入、获取和检查元素的方法。与普通的队列不同,BlockingQueue在队列为空时,获取元素的操作会被阻塞,直到队列中有可用元素为止。同样地,当队列已满时,插入元素的操作也会被阻塞,直到队列有空闲空间为止。

BlockQueue有四组API:

image-20240903161114544

BlockingQueue提供了多个实现类,其中常用的有以下几种:

  1. ArrayBlockingQueue:基于数组实现的有界阻塞队列。它在构造时需要指定队列的容量,并且在队列已满时会阻塞插入操作,直到有空闲空间。
  2. LinkedBlockingQueue:基于链表实现的可选有界或无界阻塞队列。如果构造时不指定容量,则队列大小默认为无限制。
  3. PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列。元素按照优先级进行排序,可以自定义比较器。
  4. SynchronousQueue:同步队列。是一个没有容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除/读取操作,否则进行行行插入操作的线程就要一直等待,反之亦然
    1. 进去一个元素,必须等待取出来之后,才能再往里面放入一个元素
    2. 使用lock锁保证线程安全的
  5. DelayQueue:基于优先级堆实现的延迟阻塞队列。其中的元素必须实现Delayed接口,只有经过一定时间后才能被取出。

BlockingQueue实现原理:ReentrantLock + Condition

ArrayBlockingQueueLinkedBlockingQueue区别:
- ArrayBlockingQueue使用数组作缓冲区,有界,生产者消费者之间使用独占锁,即生产者和消费者共用一把缓冲区的锁,出队和入队不能同时进行
- LinkedBlockingQueue使用链表作缓冲区,无界,生产者消费者之间使用分离锁,即生产者和消费者分别用一把缓冲区的锁,出队和入队可以同时进行

拒绝策略

如上面介绍,拒绝策略需要实现RejectedExecutionHandler接口,Executors为我们提供了4种拒绝策略:

  • AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常
  • CallerRunsPolicy:直接运行这个任务的run方法,但并非是由线程池处理,而是交由任务的调用线程处理
  • DiscardPolicy:直接丢弃任务,不抛出任何异常
  • DiscardOldestPolicy:将当前处于等待队列列头的等待任务强行取出,然后再试图将当前被拒绝的任务提交

常见线程池种类

四种线程池的特点:

  • SingleThreadExecutor单线程线程池
    • 特点:线程池只有一个线程
    • 使用一个LinkedBlockingQueue作为工作队列,未指定容量(默认值Integer.MAX_VALUE
  • FixedThreadPool固定线程池
    • 特点:最大线程数就是核心线程数,即只有核心线程
    • keepAliveTime=0,但核心线程不会被回收或者销毁
    • 无界队列:使用LinkedBlockingQueue作为工作队列,未指定容量(默认值Integer.MAX_VALUE
    • 适用于需要有一定持续并发量的场景
  • CachedThreadPool缓存线程池
    • 没有核心线程,普通线程数量无限
    • keepAliveTime=60,线程闲置60s后回收
    • 使用SynchronousQueue作为工作队列,它不会保存任务,而是直接将任务交给空闲线程执行
    • 处理大量短时间工作任务的线程池,适用于项目中多线程的场景不多或者是需要快速响应的场景,即来即处理,且用完释放,不占用过多资源
  • ScheduledThreadPool定时线程池
    • 指定核心线程数量,普通线程数量无限
    • 任务队列为延时阻塞队列DelayQueue
    • 适用用于执行定时或周期性的任务

线程池调优

在设置线程池大小时,可以考虑任务类型和系统资源的特点,以确定适当的线程池大小。具体而言,对于 CPU 密集型任务和 I/O 密集型任务,可以采取以下建议:

  • CPU 密集型任务
    • 对于 CPU 密集型任务,线程数应与 CPU 核心数相近或稍多一些,以充分利用 CPU 资源,并避免过多的线程竞争和上下文切换开销。
    • 可以通过 Runtime.getRuntime().availableProcessors() 获取当前系统的 CPU 核心数,作为线程池的核心线程数。
    • 由于 CPU 密集型任务不涉及阻塞等待,可以选择较小的工作队列容量或使用 SynchronousQueue,使得任务提交后立即执行。
  • I/O 密集型任务
    • 对于 I/O 密集型任务,一般建议将线程数设置为 CPU 核心数的几倍,例如 2 倍或 4 倍,以充分利用 I/O 操作的等待时间,提高系统的吞吐量。
    • 由于 I/O 操作会涉及到阻塞等待,可以设置较大的工作队列容量,以处理可能的任务积压。

实际线程池大小的选择还取决于任务的具体特点和系统资源的限制。在实际应用中,可以通过测试和性能调优来确定最佳的线程池大小,确保任务能够高效执行并充分利用系统资源。

线程池在业务中的实践以及对应参数如何设计