线程池
# 线程池的核心参数⭐⭐⭐
在线程池中一共有7个核心参数:
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
# 线程池的执行原理
1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行
2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列
3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务
如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务
4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略
# 线程池中有哪些常见的阻塞队列
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建临时线程执行任务
比较常见的有4个
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
用的最多是ArrayBlockingQueue和LinkedBlockingQueue
# ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
参考回答
Jdk中提供了很多阻塞队列,开发中常见的有两个:ArrayBlockingQueue
和LinkedBlockingQueue
它们在实现和使用上有一些关键的区别。
首先,ArrayBlockingQueue
是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue
默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue
是基于数组实现的,而LinkedBlockingQueue
则是基于链表实现的。这意味着ArrayBlockingQueue
在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue
则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
另外,它们在加锁机制上也有所不同。ArrayBlockingQueue
使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue
则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
# 如何确定核心线程数
① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
- IO密集型的任务 -->
- 如果有使用监控工具测算出等待时间和运行时间,可以用公式:CPU核数*(等待时间+运行时间)/运行时间,
- 而如果没有具体的数据,可以直接用 CPU核数 * 2 + 1)
- 计算密集型任务 --> ( CPU核数+1 )
③ 并发高、业务执行时间长,线程池的设置,可以参考前面的, 但是可能会导致线程池队列堆积、任务处理速度跟不上请求量。解决这种类型任务的关键不在于线程池而在于整体架构的设计,比如这些业务里面某些数据是否能做缓存,是否可以增加服务器,
# 线程池的种类有哪些
在jdk中默认提供了4中方式创建线程池
第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列 中等待。
第三个是:newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
第四个是:newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
# 为什么不建议用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()
等待计数器归零,表示任务完成。
- 使用
# 线程池使用场景
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)
# 如何控制某个方法允许并发访问线程的数量?
在jdk中提供了一个Semaphore[seməfɔːr]类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了
第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1