线程池详解:
线程池有什么用、优势?重点
线程复用:通过重复利用已创建的线程降低线程 创建 和 销毁 造成的销耗,避免反复创建销毁线程。
控制最大并发数量:线程超过了最大数量就需要排队等待。
管理线程:线程是稀缺资源, 线程池可以统一分配、监控和调优线程。
总结三点,
1 线程复用减少创建销毁线程的销毁
2 控制最大并发任务,超过了排队
3 统一管理线程
线程池应用场景?
- 异步处理任务。
- 需要快速响应请求,并行执行任务。(比如获取商品信息,要获取商品库存,商品优惠价,商品评价,这些都不在一个表中,让每个线程去执行每个任务)
- 处理大批量任务时采用 线程池并行 减少大批量任务执行时间。
为什么不推荐使用内置线程池?都会引起OOM 原因不同
- Executors.FixedThreadPool: 使用的任务队列 的容量为 Integer.MAX_VALUE无界队列,这样maximumPoolSize和keepAliveTime参数加上拒绝策略都无效,且任务多时可能导致OOM。
- Executors. SingleThreadExecutor:和FixedThreadPool一样,任务队列无界,可能导致OOM。
- Executors.CachedThreadPool:它的最大线程数maximumPoolSize被设置为 Integer.MAX.VALUE,它是无界的,可能会不断创建线程导致OOM。
- Executors.ScheduledThreadPool: 和CachedThreadPool一样,最大线程数无界。导致OOM。
Executors.FixedThreadPool有什么优点吗?虽然不推荐使用
优点以及适用场景:
特别适用于生产消费数量/任务数量稳定的场景。因为只有当任务提交速度和处理速度差不多时,才不会导致任务队列无限增长。
Java 线程池七大参数?阻塞队列有几种?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize: 池中常驻的核心线程个数。
maximumPoolSize:线程池中能够容纳同时执行的最大线程数。
keepAliveTime:非核心线程空闲存活的时间,空闲超过keepAliveTime时,多余线程销毁只剩下corePoolSize个线程。
unit:keepAliveTime的单位。
workQueue: 任务队列,被提交但尚未被执行的任务。
threadFactory:创建线程的线程工厂,一般默认。
handler:拒绝策略。有四种,当workQueue满了且当前工作线程数为maximumPoolSize,如何拒绝新来的线程。
线程池处理任务的流程了解吗?!!(重要)
- 如果当前运行的线程数小于
corePoolSize,那么就会新建一个线程来执行任务。 - 如果当前运行的线程数等于或大于
corePoolSize,但是小于maximumPoolSize,那么就把该任务放入到任务队列workQueue里等待执行。 - 如果
workQueue任务队列已经满了,但是当前运行的线程数是小于maximumPoolSize的,就新建一个线程来执行任务。 - 如果当前运行线程为
maximumPoolSize,且workQueue满了,会调用拒绝策略RejectedExecutionHandler的rejectedExecution拒绝方法。 - 如果任务不多的时候(指的任务队列中已经没有任务的情况下) ,空闲线程
maximumPoolSize-corePoolSize的存活时间超过keepAliveTime,会将多余线程销毁只剩下corePoolSize核心线程。
线程池的四种拒绝策略
AbortPolicy(默认): 中断程序 并 抛出RejectedExecutionException异常。
CallerRunsPolicy:将某些任务回退到调用者,哪来的去哪,如果是main让main执行。
DiscardPolicy:丢弃任务,且不抛异常。
DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
线程池常用的阻塞队列总结
LinkedBlockingQueue:无界队列,容量为Integer.MAX_VALUE。
SynchronousQueue: 同步队列,没有容量,不存储元素,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。(是CachedThreadPool使用的队列,这是为什么CachedThreadPool会一直创建线程的原因)
DelayedWorkQueue:延迟阻塞队列。不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序。(scheduleThreadPool使用的队列)
为什么要有最大线程和核心线程?
应对流量,核心线程应对常规流量。最大线程应对高峰流量。
使用线程池的注意事项?(美团、腾讯)
四点
- 尽量自己new ThreadPoolExecutors,不要使用内置线程池。
- 核心线程合理设置,不然消耗资源。
- 最大线程/工作队列 不要设置太大,不然可能出现OOM内存溢出。
- 用完线程池要关闭。
executor.shutdown();
线程池参数设置经验?
实际使用的时候线程池设计多大比较合理?( 腾讯云智 )
CPU密集型 (N) :将线程数设置为和CPU核心数差不多,或者是N+1。这样可以充分利用CPU的资源。
IO密集型(2N) :将线程数设置为和CPU核心数 *2N,为什么?,因为在这种任务中,系统会用很多时间处理IO操作,而在某个线程进行IO操作时,可以将CPU让出来给其他线程使用。
如何判断是CPU密集还是IO密集的任务呢?
很简单,排序等计算偏多的任务是CPU密集型。有网络IO和磁盘文件IO的是IO密集型。
如何给线程池命名?为什么建议给线程池命名?
自定义线程工厂类实现ThreadFactory接口并重写newThread方法。在newThread的方法中创建线程并设置名称。
然后创建线程池的时候 传入这个线程工厂。
为什么要命名?
有利于排查故障,定位问题。
// 自定义线程工厂 继承ThreadFactory
// 重写 newThread 方法
public final class NamingThreadFactory implements ThreadFactory{
// 原子类
private final AtomicInteger threadNum = new AtomicInteger();
private final String name;
public NamingThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(name + " [#" + threadNum.incrementAndGet() + "]");
return thread;
}
}
线程池底层原理,了解吗?
Java线程池底层原理分为两个部分。
一个部分是工作线程集合(Set),也就是Worker集合,Worker就是线程thread的封装类。
二个部分是 任务队列,任务指的是runnable,队列就是阻塞队列。
private final BlockingQueue<Runnable> workQueue;
private final HashSet<Worker> workers = new HashSet<Worker>();
线程池中运行的线程,如果出现了异常,会怎么样?(进阶)
“线程池中线程异常后:销毁还是复用?”(这篇文章很详细)
结论
execute提交到线程池的方式,如果执行中抛出异常,并且没有在执行逻辑中catch,那么会抛出异常,并且移除抛出异常的线程,创建新的线程放入到线程池中。submit提交到线程池的方式,如果执行中抛出异常,并且没有catch,不会抛出异常,不会创建新的线程(但是可以通过返回的future对象get异常信息)
源码
execute方法内部,最终会调用线程池的runWorker方法(了解) ,有try,但没有catch异常,但是在finally中进行了处理(移除线程并重新创建线程)
submit执行,submit也是调用了execute方法, 但是在调用之前,将Runnable封装为RunnableFuture(futureTask)(futureTask也实现了runnable接口)。由于被封装,将会执行futureTask的run方法,futureTask的run方法中进行了异常catch,所以不会抛出,我们也感知不到异常。但是通过future的get方法可以获取异常信息。
线程池原理,为什么线程池执行完线程的run方法后不会回收线程(进阶,问得深,大厂有可能问)
线程池中的线程为什么不会释放而是循环等待任务呢_线程池循环等待问题-CSDN博客
ThreadPoolExecutor-线程池如何保证线程不被销毁_executors.newfixedthreadpool线程执行完自动释放吗-CSDN博客
对于下面内容的总结:
我的描述:
首先通过源码要知道,在线程池中,线程Thread被封装成了一个个worker,Worker就是Thread的一个包装类。
线程池中的workQueue工作队列其实就是 存储runnable任务的队列。
Worker其实就是正在运行的线程,例如 用户执行execute方法提交了一个Runnable接口的任务
- 如果当前池子中Worker数量小于核心线程数,那么可以通过调用
addWorker方法传入runnable接口 启动并执行一个线程(调用addWorker中thread的 start() 方法) - 如果当前池子中Worker数量大于等于核心线程数, 会将任务(runnable)加入到workQueue中,等线程空闲再执行。
我们都知道,start启动一个线程后,会执行线程 中 runnable的run方法。(我们自己调用的run方法执行完毕就会回收线程,为什么线程池中的线程没有回收,那肯定跟run方法有关)
具体看看runWorker方法内部 (原来run方法是一个死循环) 。这个方法比较好懂。
- 一个大循环,判断条件是task != null || (task = getTask()) != null,task自然就是我们要执行的任务了,当task为空而且getTask()取不到任务的时候,这个while()就会结束,循环体里面进行的就是task.run();
- 这里我们其实可以打个心眼,那基本八九不离十了,肯定是这个循环一直没有退出,所以才能维持着这一个线程不断运行,当有外部任务进来的时候,循环体就能getTask()并且执行。
- 下面最后放getTask()里面的代码,验证猜想
getTask() 方法中的关键代码
getTask方法会去workQueue工作队列中获取 runnable任务,我们知道workQueue是一个阻塞队列。
对于核心Woker线程,workQueue.take方法如果获取不到会一直阻塞。
对于非核心Worker线程,workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,会退出,run方法执行完毕会回收非核心线程。