目录
- 1. 谈谈什么是线程池
- 2. 为什么要使用线程池
- 3. 你们哪些地方会使用到线程池
- 4. 线程池有哪些作用
- 5. 线程池的创建方式
- 6. 线程池底层是如何实现复用的
- 7. ThreadPoolExecutor 核心参数有哪些
- 8. 线程池创建的线程会一直在运行状态吗?
- 9. 为什么阿里巴巴不建议使用 Executors
- 10. 线程池底层 ThreadPoolExecutor 底层实现原理
- 11. 线程池队列满了,任务会丢失吗
- 12. 线程池拒绝策略类型有哪些呢
- 13. 线程池如何合理配置参数
1. 谈谈什么是线程池
线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。
2. 为什么要使用线程池
因为频繁的开启线程或者停止线程,线程需要从新被 cpu 从就绪到运行状态调度,需要发生cpu 的上下文切换,效率非常低。
3. 你们哪些地方会使用到线程池
新用户注册:用户注册成功后,异步的去发短信通知、邮件通知、发送优惠券
实际开发项目中 禁止自己 new 线程。
必须使用线程池来维护和创建线程。
4. 线程池有哪些作用
核心点:复用机制 提前创建好固定的线程一直在运行状态 实现复用限制线程创建数量。
- 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
- 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池 ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
5. 线程池的创建方式
Executors.newCachedThreadPool(); 可缓存线程池
Executors.newFixedThreadPool();可定长度 限制最大线程数
Executors.newScheduledThreadPool() ; 可定时
Executors.newSingleThreadExecutor(); 单例
底层都是基于 ThreadPoolExecutor 构造函数封装
6. 线程池底层是如何实现复用的
本质思想:创建一个线程,不会立马停止或者销毁而是一直实现复用。
- 提前创建固定大小的线程一直保持在正在运行状态;(可能会非常消耗cpu 的资源)
- 当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略;
- 正在运行的线程从并发队列中获取任务执行从而实现多线程复用问题;
线程池核心点:复用机制
- 提前创建好固定的线程一直在运行状态----死循环实现
- 提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行
- 正在运行的线程就从队列中获取该任务执行
简单模拟手写 Java 线程池:
public class MyExecutors {
public BlockingDeque<Runnable> runnables;
private volatile Boolean isRun = true;
/**
* dequeSize 缓存任务大小
*
* @param dequeSize
* @param threadCount 复用线程池
*/
public MyExecutors(int dequeSize, int threadCount) {
runnables = new LinkedBlockingDeque<Runnable>(dequeSize);
for (int i = 0; i < threadCount; i++) {
WorkThread workThread = new WorkThread();
workThread.start();
}
}
public void execute(Runnable runnable) {
runnables.offer(runnable);
}
class WorkThread extends Thread {
@Override
public void run() {
while (isRun||runnables.size()>0) {
Runnable runnable = runnables.poll();
if (runnable != null) {
runnable.run();
}
}
}
}
public static void main(String[] args) {
MyExecutors myExecutors = new MyExecutors(10, 2);
for (int i = 0; i < 10; i++) {
final int finalI = i; myExecutors.execute(
new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":," + finalI);
}
});
}
myExecutors.isRun=false;
}
}
7. ThreadPoolExecutor 核心参数有哪些
corePoolSize:核心线程数量 一直正在保持运行的线程
maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
keepAliveTime:超出 corePoolSize 后创建的线程的存活时间。
unit:keepAliveTime 的时间单位。
workQueue:任务队列,用于保存待执行的任务。
threadFactory:线程池内部创建线程所用的工厂。
handler:任务无法执行时的处理器。
8. 线程池创建的线程会一直在运行状态吗?
不会
例如:配置核心线程数 corePoolSize 为 2 、最大线程数 maximumPoolSize 为5
我们可以通过配置超出 corePoolSize 核心线程数后创建的线程的存活时间例如为60s
在 60s 内没有核心线程一直没有任务执行,则会停止该线程。
9. 为什么阿里巴巴不建议使用 Executors
因为默认的 Executors 线程池底层是基于 ThreadPoolExecutor
构造函数封装的,采用无界队列存放缓存任务,会无限缓存任务容易发生内存溢出,会导致我们最大线程数会失效。
10. 线程池底层 ThreadPoolExecutor 底层实现原理
- 当线程数小于核心线程数时,创建线程。
- 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
- 当线程数大于等于核心线程数,且任务队列已满
3.1 若线程数小于最大线程数,创建线程
3.2 若线程数等于最大线程数,抛出异常,拒绝任务
11. 线程池队列满了,任务会丢失吗
如果队列满了,且任务总数>最大线程数则当前线程走拒绝策略。
可以自定义异拒绝异常,将该任务缓存到 redis、本地文件、mysql 中后期项目启动实现补偿。
1.AbortPolicy 丢弃任务,抛运行时异常
2.CallerRunsPolicy 执行任务
3.DiscardPolicy 忽视,什么都不会发生
4.DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
5.实现 RejectedExecutionHandler 接口,可自定义处理器
12. 线程池拒绝策略类型有哪些呢
- AbortPolicy 丢弃任务,抛运行时异常
- CallerRunsPolicy 执行任务
- DiscardPolicy 忽视,什么都不会发生
- DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
- 实现 RejectedExecutionHandler 接口,可自定义处理器
13. 线程池如何合理配置参数
自定义线程池就需要我们自己配置最大线程数 maximumPoolSize,为了高效的并发运行,当然这个不能随便设置。这时需要看我们的业务是 IO 密集型还是 CPU 密集型。
CPU 密集型
CPU 密集的意思是该任务需要大量的运算,而没有阻塞,CPU 一直全速运行。CPU密集任务只有在真正的多核 CPU 上才可能得到加速(通过多线程),而在单核CPU 上,无论你开几个模拟的多线程该任务都不可能得到加速,因为 CPU 总的运算能力就那些。
CPU 密集型任务配置尽可能少的线程数量:以保证每个 CPU 高效的运行一个线程。一般公式:(CPU 核数+1)个 线程的线程池
IO 密集型
I0 密集型,即该任务需要大量的 IO,即大量的阻塞。在单线程上运行I0 密集型的任务会导致浪费大量的 CPU 运算能力浪费在等待。
所以在 IO 密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
I0 密集型时,大部分线程都阻寒,故需要多配置线程数:
公式:
CPU 核数 * 2
CPU 核数 / (1 - 阻塞系数) 阻塞系数 在 0.8~0.9 之间
查看 CPU 核数:
System.out.println(Runtime.getRuntime().availableProcessors());