面试官:讲一讲你用过的线程池

115 阅读6分钟

前言

线程池,老生常谈了,也是面试必问,这个工作中还经常遇到,背一背不亏

image.png 线程池是一种多线程处理形式,将任务提交到线程池,任务的执行交由线程池来管理。线程池提供了线程队列,队列中保存了所有等待状态的线程。

线程池的特点是:线程复用;控制最大并发数;管理线程的生命周期

顶层是 Executor接口,其只有一个 execute()方法,其他的主要类还有:ThreadPoolExecutor,是主要的线程池的实现类,负责线程调度。

如下图所示

ExecutorService继承了Executor接口

ScheduledExecutorService继承了ExecutorService接口

ScheduledThreadPoolExecutor继承了 ThreadPoolExecutor类,实现了 ScheduledExecutorService

AbstractExecutorService 抽象类实现了ExecutorService接口

另外还有一个 Executors工具类,可以用来创建线程池,但是不推荐使用

image-20230920193753292.png

线程池的作用

  1. 提高效率。我们知道线程是非常宝贵的系统资源,创建和销毁都需要时间的。有了线程池,就相当于了有个大的池子,池子里有创建好的线程,使用的时候直接从池子里拿就可以,用完也不用自己销毁,再放到池子里即可
  2. 方便管理。使用线程池可以控制线程的数量,避免将服务器资源用尽。当有20个任务需要执行时,线程池的最大大小是10,那么剩下的十个任务就会被放到队列中,排队等候执行,从而避免了无休止的创建线程导致系统崩溃

线程的参数

我们看一下 ThreadPoolExecutor 源码中的构造函数的参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
​

可以看到共有七个参数;分别是:

corePoolSize 线程池中的常驻核心线程数

maximumPoolSize 线程池最大同时执行的线程数

keepAliveTime,空闲线程(包括核心线程和非核心线程)存活时间,当空闲时间达到 keepAliveTime 值时,多余的线程会被销毁,只剩下corePoolSize 个核心线程

核心线程和非核心线程都可能被回收,最终留下corePoolSize 个线程,这些作为新的核心线程,所以核心线程的数量会一直保持不变,但具体的核心线程可能会因为到达最大空闲时间被其他非核心线程替换。

(感谢我硕哥的批评指正,给大佬递茶)

image.png

unit,keepAliveTime的单位

workQueue,任务队列,被提交但尚未执行的任务

threadFactory,表示生成线程池的工作线程的线程工厂,用户创建新线程,一般使用默认即可

handler,拒绝策略,表示当前线程队列满了并且工作线程大于线程池的最大数量maximumPoolSize 时,该如何处理

例子:

假设corePoolSize 大小为10,maximumPoolSize 大小为20,workQueue队列的大小为50

现在有100个任务要执行,前10个任务进来,会直接从线程池中拿到线程,

11-60个任务进来时,线程池发现核心线程已经用完了,且队列还没满,此时会将任务放到任务队列中,将 11-60个任务放到队列中

当第61-70个任务进来时,此时核心线程已满,队列已满,线程池目前尚未达到最大线程数量,此时会创建新的线程,创建10个线程,用于第61-70个任务执行

当第71-100个任务进来时,线程池目前仍未有释放的线程,且队列也已满,那么就会执行拒绝策略 handler

image.png

拒绝策略

根据上面的例子,当队列也满了的时候,会执行拒绝策略。拒绝策略共有四种,分别是:

AbortPolicy(默认):丢弃任务并抛出 RejectedExecutionException 异常

CallerRunsPolicy:由调用线程处理该任务

DiscardPolicy:丢弃任务,但是不抛出异常

DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务

常见的线程池

常见的线程池共有四种,可以通过 Executors工具类来创建,但不推荐, 分别是:newSingleThreadExecutornewFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPool

newSingleThreadExecutor

主要特征如下:创建一个单线程的线程池,它会用唯一的工作线程来执行任务,保证所有任务都能按照顺序执行

newFixedThreadPool

  1. 创建一个定长的线程池,可控制线程的最大并发数,超出的线程会在队列中等待
  2. 使用的是LinkedBlockingQueue

newCachedThreadPool

  1. 创建一个可缓存的线程池,如果线程池超过处理需要,则灵活回收空闲线程,若无可回收,则创建新线程
  2. 它将corePoolSize 设置为了0,将maxnumPoolSize设置为了 Integer.MAX_VALUE,他使用的是SynchronousQueue,如果线程空闲超过60秒,就回收线程

newScheduleThreadPool

创建一个定长线程池,支持定时及周期性任务执行

工作中使用哪一种线程池?

答案是:上面的一个都不用

image.png

根据阿里巴巴开发手册规范

【强制】线程资源必须通过线程池提供,不允许在应用中自行显示创建

【强制】线程池不允许使用 Executors去创建,而是通过 ThreadPoolExecutor的方式,这样的处理方式可以让写的同学更加明确线程池的运行规则,避免资源耗尽的风险

上面四种自带的线程池的缺点:

  1. newFixedThreadPoolnewSingleThreadExecutor,其使用的 LinkedBlockingQueue允许的最大队列为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM

        public LinkedBlockingQueue() {
            this(Integer.MAX_VALUE);
        }
    
  2. newCachedThreadPoolnewScheduledThreadPool ,允许创建的最大线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

        public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    
        public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
                  new DelayedWorkQueue());
        }
    

image.png

线程池的关闭

可以使用 shutdownNowshutdown两个方法来实现

不同的是,shutdownNow会停止所有任务,而shutdown是不再接受新的任务,但也不会去强制终止已经提交或者正在运行的任务

线程数量配置

CPU密集时:可以配置 cpu核数+1个线程

IO密集时:2 * CPU核数

总结提高

本文介绍学习了线程池的常用参数,拒绝策略,及常用的线程池,通过学习可以面对初级的面试,可以在工作中解决基本的问题。不急不馁,慢慢成长