【JUC并发】八股总结四:线程池

77 阅读9分钟

线程池详解:

JUC&JVM

线程池有什么用、优势?重点

线程复用:通过重复利用已创建的线程降低线程 创建 销毁 造成的销耗,避免反复创建销毁线程。

控制最大并发数量:线程超过了最大数量就需要排队等待。

管理线程:线程是稀缺资源, 线程池可以统一分配、监控和调优线程。


总结三点,

1 线程复用减少创建销毁线程的销毁

2 控制最大并发任务,超过了排队

3 统一管理线程

线程池应用场景?

✅ 线程池有哪些应用场景?

  1. 异步处理任务。
  2. 需要快速响应请求,并行执行任务。(比如获取商品信息,要获取商品库存,商品优惠价,商品评价,这些都不在一个表中,让每个线程去执行每个任务)

  1. 处理大批量任务时采用 线程池并行 减少大批量任务执行时间

为什么不推荐使用内置线程池?都会引起OOM 原因不同

  1. Executors.FixedThreadPool: 使用的任务队列 的容量为 Integer.MAX_VALUE无界队列,这样maximumPoolSize和keepAliveTime参数加上拒绝策略都无效,且任务多时可能导致OOM。
  2. Executors. SingleThreadExecutor:和FixedThreadPool一样,任务队列无界,可能导致OOM。

  1. Executors.CachedThreadPool:它的最大线程数maximumPoolSize被设置为 Integer.MAX.VALUE,它是无界的,可能会不断创建线程导致OOM。
  2. Executors.ScheduledThreadPool: 和CachedThreadPool一样,最大线程数无界。导致OOM。

Executors.FixedThreadPool有什么优点吗?虽然不推荐使用

✅ 为什么不建议Executors创建线程池?

优点以及适用场景:

特别适用于生产消费数量/任务数量稳定的场景。因为只有当任务提交速度和处理速度差不多时,才不会导致任务队列无限增长。

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,如何拒绝新来的线程。

线程池处理任务的流程了解吗?!!(重要)

  1. 如果当前运行的线程数小于corePoolSize,那么就会新建一个线程来执行任务。
  2. 如果当前运行的线程数等于或大于corePoolSize,但是小于maximumPoolSize,那么就把该任务放入到任务队列workQueue里等待执行。
  3. 如果workQueue任务队列已经满了,但是当前运行的线程数是小于maximumPoolSize的,就新建一个线程来执行任务。
  4. 如果当前运行线程为maximumPoolSize,且workQueue满了,会调用拒绝策略RejectedExecutionHandler的rejectedExecution拒绝方法。
  5. 如果任务不多的时候(指的任务队列中已经没有任务的情况下) ,空闲线程maximumPoolSize - corePoolSize的存活时间超过 keepAliveTime,会将多余线程销毁只剩下corePoolSize核心线程。

线程池的四种拒绝策略

AbortPolicy(默认): 中断程序 并 抛出RejectedExecutionException异常

CallerRunsPolicy:将某些任务回退到调用者,哪来的去哪,如果是main让main执行。

DiscardPolicy丢弃任务,且不抛异常

DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。

线程池常用的阻塞队列总结

LinkedBlockingQueue无界队列,容量为Integer.MAX_VALUE。

SynchronousQueue: 同步队列没有容量,不存储元素,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。(是CachedThreadPool使用的队列,这是为什么CachedThreadPool会一直创建线程的原因)

DelayedWorkQueue延迟阻塞队列不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序。(scheduleThreadPool使用的队列)

为什么要有最大线程和核心线程?

应对流量,核心线程应对常规流量最大线程应对高峰流量

使用线程池的注意事项?(美团、腾讯)

四点

  1. 尽量自己new ThreadPoolExecutors,不要使用内置线程池
  2. 核心线程合理设置,不然消耗资源。
  3. 最大线程/工作队列 不要设置太大,不然可能出现OOM内存溢出。
  4. 用完线程池要关闭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>();

线程池中运行的线程,如果出现了异常,会怎么样?(进阶)

“线程池中线程异常后:销毁还是复用?”(这篇文章很详细)

结论

  1. execute 提交到线程池的方式,如果执行中抛出异常,并且没有在执行逻辑中catch那么会抛出异常,并且移除抛出异常的线程,创建新的线程放入到线程池中
  2. submit 提交到线程池的方式,如果执行中抛出异常,并且没有catch,不会抛出异常,不会创建新的线程(但是可以通过返回的future对象get异常信息)

源码

  1. execute 方法内部,最终会调用线程池的runWorker方法(了解) ,有try,但没有catch异常但是在finally中进行了处理移除线程并重新创建线程

  1. submit 执行,submit也是调用了execute方法, 但是在调用之前,将Runnable封装为RunnableFuture(futureTask)(futureTask也实现了runnable接口)。由于被封装,将会执行futureTask的run方法futureTask的run方法中进行了异常catch,所以不会抛出,我们也感知不到异常。但是通过future的get方法可以获取异常信息。


线程池原理,为什么线程池执行完线程的run方法后不会回收线程(进阶,问得深,大厂有可能问)

线程池中的线程为什么不会释放而是循环等待任务呢_线程池循环等待问题-CSDN博客

ThreadPoolExecutor-线程池如何保证线程不被销毁_executors.newfixedthreadpool线程执行完自动释放吗-CSDN博客

Java 线程池详解

对于下面内容的总结:

我的描述:

首先通过源码要知道,在线程池中,线程Thread被封装成了一个个worker,Worker就是Thread的一个包装类。

线程池中的workQueue工作队列其实就是 存储runnable任务的队列。

Worker其实就是正在运行的线程,例如 用户执行execute方法提交了一个Runnable接口的任务

  1. 如果当前池子中Worker数量小于核心线程数,那么可以通过调用addWorker方法传入runnable接口 启动并执行一个线程(调用addWorker中thread的 start() 方法)
  2. 如果当前池子中Worker数量大于等于核心线程数, 会将任务(runnable)加入到workQueue中,等线程空闲再执行。

我们都知道,start启动一个线程后,会执行线程 中 runnable的run方法。(我们自己调用的run方法执行完毕就会回收线程,为什么线程池中的线程没有回收,那肯定跟run方法有关)

具体看看runWorker方法内部 (原来run方法是一个死循环) 。这个方法比较好懂。

  1. 一个大循环,判断条件是task != null || (task = getTask()) != nulltask自然就是我们要执行的任务了,当task为空且getTask()取不到任务的时候,这个while()就会结束,循环体里面进行的就是task.run();
  2. 这里我们其实可以打个心眼,那基本八九不离十了,肯定是这个循环一直没有退出,所以才能维持着这一个线程不断运行当有外部任务进来的时候,循环体就能getTask()并且执行
  3. 下面最后放getTask()里面的代码,验证猜想

getTask() 方法中的关键代码

getTask方法会去workQueue工作队列中获取 runnable任务,我们知道workQueue是一个阻塞队列

对于核心Woker线程,workQueue.take方法如果获取不到会一直阻塞。

对于非核心Worker线程,workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) ,如果超时还没有拿到,会退出,run方法执行完毕会回收非核心线程