线程池
# 线程池的核心参数⭐⭐⭐
在线程池中一共有7个核心参数:
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
# 线程池的执行原理
1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略
# 阻塞队列
阻塞队列是一种线程安全的集合,它在基础队列操作上增加了两种关键特性:
- 当队列为空时,消费者线程会被阻塞,并自动唤醒后续的生产者线程
- 当队列满时,生产者线程会被阻塞,并自动唤醒后续的消费者线程
阻塞队列从容量维度可分为:
- 有界队列,如ArrayBlockingQueue,构造时需指定固定容量
- 而无界队列就是没有设置固定大小的队列,如LinkedBlockingQueue(默认Integer.MAX_VALUE),吞吐量更高但存在OOM风险
典型应用场景:
- 线程池任务缓冲(如ThreadPoolExecutor的工作队列)
- 生产者-消费者模式实现
- 消息中间件的内部缓冲
# 线程池中有哪些常见的阻塞队列
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建临时线程执行任务
比较常见的有4个
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
用的最多是ArrayBlockingQueue和LinkedBlockingQueue
# 基于数组的阻塞队列ArrayBlockingQueue的理解
阻塞队列是一种线程安全的集合,它在基础队列操作上增加了两种关键特性:
- 当队列为空时,消费者线程会被阻塞,并自动唤醒后续的生产者线程
- 当队列满时,生产者线程会被阻塞,并自动唤醒后续的消费者线程
实现这样一个阻塞队列,需要用到两个非常关键的技术 队列元素的存储和线程的阻塞和唤醒, 而ArrayBlockingQueue是基于数组结构的有界阻塞队列,也就是说队列元素是存储在一个数组结构里面,并且这个数组的长度是有限制的,为了达到循环生产和循环消费这样一个目的呢,ArrayBlockingQueue里面用到了一个循环数组,
而线程的阻塞和唤醒,用到了JUC包里面的一个ReentrantRock和Condition,Condition相当于wait和notify在JUC里面的一个实现,
# ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
参考回答
Jdk中提供了很多阻塞队列,开发中常见的有两个:ArrayBlockingQueue
和LinkedBlockingQueue
它们在实现和使用上有一些关键的区别。
首先,ArrayBlockingQueue
是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue
默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue
是基于数组实现的,而LinkedBlockingQueue
则是基于链表实现的。这意味着ArrayBlockingQueue
在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue
则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
另外,它们在加锁机制上也有所不同。ArrayBlockingQueue
使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue
则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
# 阻塞队列被异步消费,是如何去保证消费的顺序
阻塞队列(如 ArrayBlockingQueue
、LinkedBlockingQueue
)通过 FIFO(先进先出) 特性保证任务的存储和消费顺序。在多线程异步消费场景下,其顺序性主要通过以下机制实现:
首先阻塞队列啊,本身是一个符合FIFO特性的队列,也就是说存储进去的元素,它是符合先进先出的规则,
阻塞队列通过 锁机制 和 条件等待队列 协调生产者和消费者
阻塞队列非空时
- 多个消费者线程通过竞争锁获取任务,但每次仅有一个线程成功取走队列头部任务,保证消费顺序与入队顺序一致。
阻塞队列中维护了两个条件变量:notEmpty
(非空条件)和 notFull
(非满条件),
- 队列为空时:所有消费者线程按 FIFO 顺序 在
notEmpty
等待队列中阻塞。 - 当新任务入队时:唤醒
notEmpty
队列中最早阻塞的线程,确保先等待的线程优先消费,从而维持消费顺序。
# 如何确定核心线程数
① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
- IO密集型的任务 -->
- 如果有使用监控工具测算出等待时间和运行时间,可以用公式:CPU核数*(等待时间+运行时间)/运行时间,
- 而如果没有具体的数据,可以直接用 CPU核数 * 2 + 1)
- 计算密集型任务 --> ( CPU核数+1 )
③ 并发高、业务执行时间长,线程池的设置,可以参考前面的, 但是可能会导致线程池队列堆积、任务处理速度跟不上请求量。解决这种类型任务的关键不在于线程池而在于整体架构的设计,比如这些业务里面某些数据是否能做缓存,是否可以增加服务器,
# 线程池的种类有哪些
在jdk中默认提供了五种方式创建线程池
第一个是:newCachedThreadPool创建一个可缓存线程池,它的阻塞队列是一个SynchronousQueue,是不能存储任何元素的阻塞队列,每提交一个任务都需要立即分配一个工作线程来处理,空闲线程的存活周期是60秒,当线程的任务执行完成,这些空闲线程可以缓存起来,去应对接下来的任务的处理。
第二个是:newFixedThreadPool 创建一个固定线程数量的线程池,他的核心线程数等于最大线程数,使用无界队列,超出的线程会在队列中等待,适合长期稳定负载,但队列过长可能导致 OOM
第三个是:newScheduledThreadPool 支持延迟执行和周期性任务。
第四个是:newSingleThreadExecutor 仅 1 个工作线程,任务按 FIFO 顺序执行。保证任务串行化,避免并发问题
第五个是:newWorkStealingPool,它是java8里面提供的一个线程池,它内部会去构建一个ForkJoinPool,它会利用一个工作窃取的算法,让空闲线程窃取其他队列任务,提高吞吐量
所有线程池最终通过 ThreadPoolExecutor
实现
newCachedThreadPool
和newFixedThreadPool
可能因任务堆积导致 OOM。- 实际开发中推荐通过
ThreadPoolExecutor
构造函数显式设置参数(如队列容量、拒绝策略)。
# 为什么不建议用Executors创建线程池
主要原因是如果使用Executors创建线程池的话,它的阻塞队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的任务请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。
# 线程池如何知道线程任务的执行完成
- 线程池的isTerminated()
- 使用isTerminated()方法可以检查线程池是否已经完全终止,但需要先调用
shutdown()
方法。通常情况下,线程池不会随意关闭,因此isTerminated()
的使用场景有限。
- 使用isTerminated()方法可以检查线程池是否已经完全终止,但需要先调用
- 使用Future对象
- Future可以判断单个任务的完成状态。通过调用线程池的
submit()
方法提交任务时,会返回一个Future
对象,而Future
的isDone()
方法,可以非阻塞地检查任务是否完成。或者调用Future的get()方法可以阻塞当前线程,直到任务完成并返回结果或者抛出异常。
- Future可以判断单个任务的完成状态。通过调用线程池的
- CountDownLatch:
- 使用
CountDownLatch
同步工具,适合用于多个任务的同步,可以在开始时设置一个初始值,任务完成后调用countDown()
方法把计数器减一,主线程通过await()
等待计数器归零,表示任务完成。
- 使用
# 线程池是如何线程复用的
线程池通过生产者-消费者模型和阻塞队列实现线程复用。
生产者消费者模型,其实就是通过一个中间容器来去解耦,生产者不断的生产任务保存到容器里面,消费者不断从容器里面去消费任务,
在线池里面就使用了阻塞队列,生产线程不断地往线程池里面去传递任务,这些任务会保存到线程池的一个阻塞队列里面,而线程池里的工作线程不断从阻塞队列获取任务执行
基于阻塞队列的特性,使得阻塞队列里没有任何任务时,工作线程就会阻塞等待,直到又有新的任务进来的时候,那么这些工作线程又再次被唤醒,从而去达到现成的一个复用的目的
# 线程池是如何进行线程回收的
首先线程池里面分为核心线程和非核心线程,核心线程是线程池的常驻工作线程,有两种方式来初始化,
- 提交任务时,若当前线程数 <
corePoolSize
,则创建新核心线程。 - 调用
prestartCoreThread()
方法提前创建,
当线程池里的阻塞队列满时,线程池会增加非核心线程以提升处理能力,核心线程和非核心线程的数量,是在构造线程池的时候去设置的,也可以动态进行更改,
当任务处理完成后,非核心线程处于空闲状态,就需要进行回收,通过阻塞队列的 poll(timeout, unit)
方法指定超时时间和超时时间单位,在指定超时时间内未获取到新任务(即队列为空),poll
返回 null
,就会终止当前的线程,完成线程的回收
默认情况下呢,线程池只会回收非核心线程, 如果希望核心线程也能回收,可以将allowCoreThreadTimeOut属性设置为true, 一般情况下我们不会去回收核心线程,线程池的核心目的是复用线程,减少创建/销毁开销。 而且核心线程在没有任务处理时,处于阻塞状态,并没有占用CPU资源,
# 当任务数超过线程池的核心线程数量时如何不让它进入队列,而是直接去启用最大线程数量
线程池的默认工作流程
- 任务优先由核心线程处理。
- 若核心线程全忙,新任务进入阻塞队列(如
LinkedBlockingQueue
)。 - 仅当队列满时,才启动新线程(直至达到最大线程数)。
- 若线程数已达最大值且队列满,触发拒绝策略(如
AbortPolicy
)。
想要迫使线程池直接创建非核心线程,就需要将阻塞队列替换为无法存储任务的队列(如SynchronousQueue
)
而java线程池的构造方法有一个参数可以修改阻塞队列类型,其中有一个阻塞队列SynchronousQueue
。这个队列它是不能存储任何元素的,每生产一个任务,就必须指还有一个消费者来处理这个任务,否则就会阻塞生产者,
# 线程池使用场景
CountDownLatch、Future
https://www.bilibili.com/video/BV1yT411H7YK/?p=111
https://heuqqdmbyk.feishu.cn/wiki/MUL1wejrviNhsxklCwKcGk01nzh
嗯~~,我想一下当时的场景[根据自己简历上的模块设计多线程场景]
参考场景一:
es数据批量导入
在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以,当时我就想到可以使用线程池的方式导入,利用CountDownLatch+Future来控制,就能大大提升导入的时间。
参考场景二:
在我做那个xx电商网站的时候,里面有一个数据汇总的功能,在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终再汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行
参考场景三:
《黑马头条》项目中使用的
我当时做了一个文章搜索的功能,用户输入关键字要搜索文章,同时需要保存用户的搜索记录(搜索历史),这块我设计的时候,为了不影响用户的正常搜索,我们采用的异步的方式进行保存的,为了提升性能,我们加入了线程池,也就说在调用异步方法的时候,直接从线程池中获取线程使用(/使用MQ)
# 简述以下对线程池的理解
线程池是池化技术(Pooling)的典型应用,而池化技术是一种资源复用的设计思想,比较常见的池化技术有连接池,内存池和对象池,而线程池里面复用的是线程资源,
它的核心设计目的有两个,
- 可以减少线程的频繁创建和销毁,带来的性能开销,因为线程创建会涉及到CPU的上下文切换,以及内存的分配这样一些工作,
- 线程池本身啊,会有一些参数来控制线程的创建数量,可以去避免无休止的创建线程,带来的资源利用率过高的问题,所以它可以起到资源保护的一个作用,
线程的生命周期是由任务的运行状态来决定的,无法去人为控制,所以为了实现线程的复用,线程池里面的用到了阻塞队列,简单来说,就是线程池里的工作线程处于一直运行状态,他会去从阻塞队列里面去获取待执行的任务,一旦队列空了,工作线程就会被阻塞,直到下一次有新的任务进来,也就是说,工作线程是根据任务的情况来决定阻塞和唤醒,从而达到线程复用的目的,
最后线程池里面的资源限制,是通过几个关键参数来控制的,分别是核心线程数和最大线程数量,核心线程数表示默认长期存在的工作线程,而最大的线程数,是根据任务的情况来动态创建的线程,它的主要目的是为了提高阻塞队列中任务的处理效率,
# 如何控制某个方法允许并发访问线程的数量?
在jdk中提供了一个Semaphore[seməfɔːr]类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了
第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1