计算机系统应用教程网站

网站首页 > 技术文章 正文

互联网大厂面试系列-面试中被问到Java线程池核心参数有哪些?

btikc 2024-10-22 10:31:08 技术文章 6 ℃ 0 评论

无论是我们的日常开发中,还是在面试中,多线程都是我们避免不了的一个话题,而问到多线程相关问题,就不得不提及线程优化,而提到线程优化就不得不提到线程池相关的内容。下面我们就来聊一聊关于Java线程池相关的内容。

线程池是什么?

首先来讲,线程池是一种使用了池化技术思想来管理线程的工具,被用来管理多线程相关的内容。在这之前我们了解最多的池化思想使用的场景就是数据库连接池,说白了数据库连接池其实就可以看做一个线程池。

线程的创建、调度、销毁都是很占用资源的操作。而多线程的使用就更加去浪费这一部分资源,由于多线程的加入,会极大的影响计算机整体的性能表现。这个时候就出现了线程池化技术,通过线程池技术可以同时维护多个线程的资源,从而达到多线程资源之间的相互高效利用。这种做法一方面是避免了任务处理的时候对于创建线程、销毁线程所带来的开销,另一方面也可以控制线程膨胀带来的CPU过度调度的问题,可以保证内核的充分利用。

线程池用来解决什么问题?

根据上面的介绍线程池所解决的核心问题就是对于线程资源的管理问题。现在的系统设计都离不开并发场景的考虑。但是在并发场景中系统却不能确定在什么时候会有多少线程需要去执行,而这种不确定性所带的问题有如下一些。

  • 多线程的频繁创建、销毁,任务执行,线程上下文的切换等都会带来很大的内存开销,影响应用的正常使用。
  • 如果出现了大量的线程无法释放,线程指数增长,就会导致资源无法申请,因为我们知道线程的创建也是需要空间的,如果创建的线程过多而没有正常释放的话就会导致内存溢出等问题,导致应用无法正常使用。
  • 系统资源无法得到高效的利用,有时候有些耗时操作会占用CPU大量的时间导致其他线程由于各种资源竞争问题而无法得到有效执行,从而导致系统运行不稳定,功能时好时坏。

为了解决这些问题,就有人提出了池化思想,顾名思义就是通过一个统一的管理池来对线程资源进行有效的管理。而池化思想比较经典的使用就是线程池、数据库连接池、常量池等等。

线程池的核心组件和核心类

说到线程池,主要是由如下的四个核心组件构成

  • 线程池管理器:由用户创建用来进行线程的管理
  • 工作线程:在线程池中用来执行具体任务的线程
  • 任务接口:用来定义工作线程需要调度和执行的任务策略,只有实现了对应的接口,任务才会被线程池所调度。
  • 任务队列:用来存放待处理任务的队列,新的待执行任务将会被不断的加入到该队列中,执行完成的任务将会从队列中被移除。

Java中的线程池是通过Executor框架来实现,其中包括了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable、Future、FutureTask等核心类实现。其类继承关系如下图所示

其中ThreadPoolExecutor实现的顶层接口就是Executor,而Executor所提供的设计思想就是将任务的提交与任务的执行进行了解耦操作,使用者可以不需要关心线程是如何被创建,如何进行任务调度,如何来进行线程的销毁等。而是只需要关心执行什么样的业务逻辑,通过继承Runnable接口来实现具体的业务逻辑并且提交到执行器Executor即可,剩下的操作则是由Executor来完成。

在ExecutorService中,还增加了一部分的能力。第一、就是对执行任务能力的扩展,补充了可以为一个或者一批异步任务生成一个Future的方法;第二、提供了线程池管控的方法,例如停止线程池执行等。

在AbstractExecutorService 中 则是将所有的线程池执行操作进行了串联,保证了在下层执行中只需要调用一个方法就可以将整个的线程池的执行流程串联起来。

而在ThreadPoolExecutor中则是实现了一些复杂的调用。一方面维护了自身运行的生命周期,另一方面就是对线程池的任务的生命周期进行管理了管理。其运行机制如下图所示。

ThreadPoolExecutor 详解

根据上面介绍ThreadPoolExecutor作为线程池的核心类,它有如下的一个构造方法。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

下面我们就来介绍一下构造方法中的一些核心参数。

  • corePoolSize :核心线程数,当任务提交到线程池的时候,如果线程池中线程的数量还没有达到corePoolSize的值,那么就会新创建一个线程来执行该任务,如果达到了就将任务添加到任务等待队列中。
  • maximumPoolSize:最大线程数:当任务队列满了之后,如果再有新的线程进入到线程池,这个时候就会判断如果新建一个线程是否会超过maximumPoolSize的值,如果会超过,那么就不会创建线程,然后去执行拒绝策略,如果不会超过,则会创建一个新的线程来执行该任务。
  • keepAliveTime:当线程池中的线程大于corePoolSize的时候,那么大于corePoolSize这个部分的线程如果没有对应的任务需要去处理,那么就表示这些线程进入到了一个空闲的状态,这个时候线程池为了节省资源是不会允许这些线程存在的,只会允许它们等待一段时间之后就进行销毁了,而这个指定的空闲等待时间就是keepAliveTime值。其单位就是由TimeUnit来确定。
  • unit:空闲线程等待时间单位,就是上面的这个TimeUnit,它是一个枚举类型,用来表示时间单位。
  • workQueue:任务队列,这个队列就是上面我们提到的用来存放待执行任务。该队列的类型是阻塞队列,常用的阻塞队列有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue等。
  • threadFactory:线程池工厂,用来创建线程。通常在实际项目中,为了便于后期排查问题,在创建线程时需要为线程赋予一定的名称,通过线程池工厂,可以方便的为每一个创建的线程设置具有业务含义的名称。
  • handler:拒绝策略。当任务队列已满,线程数量达到maximumPoolSize后,线程池就不会再接收新的任务了,这个时候就需要使用拒绝策略来决定最终是怎么处理这个任务。默认情况下使用AbortPolicy,表示无法处理新任务,直接抛出异常。在ThreadPoolExecutor类中定义了四个内部类,分别表示四种拒绝策略。我们也可以通过实现RejectExecutionHandler接口来实现自定义的拒绝策略。

任务队列介绍

  • ArrayBlockingQueue是一个基于数组实现的阻塞队列,元素按照先进先出(FIFO)的顺序入队、出队。因为底层实现是数组,数组在初始化时必须指定大小,因此ArrayBlockingQueue是有界队列。
  • LinkedBlockingQueue是一个基于链表实现的阻塞队列,元素按照先进先出(FIFO)的顺序入队、出队。因为顶层是链表,链表是基于节点之间的指针指向来维持前后关系的,如果不指链表的大小,它默认的大小是Integer.MAX_VALUE,即,这个数值太大了,因此通常称LinkedBlockingQueue是一个无界队列。当然如果在初始化的时候,就指定链表大小,那么它就是有界队列了。
  • SynchronousQueue是一个不存储元素的阻塞队列。每个插入操作必须得等到另一个线程调用了移除操作后,该线程才会返回,否则将一直阻塞。吞吐量通常要高于LinkedBlockingQueue。
  • PriorityBlockingQueue是一个将元素按照优先级排序的阻塞的阻塞队列,元素的优先级越高,将会越先出队列。这是一个无界队列。

线程池执行流程

如图所示,线程池的使用执行流程。其主要有三大核心执行策略。如下

  1. 首先判断线程池中的线程数是否大于核心线程数,如果没有超过核心线程数,那么就需要创建新的线程来执行任务,如果超过了核心线程数,那么就进入到队列检查
  2. 判断队列是否满了,如果没有满,就将执行任务添加到队列中进行等待,如果满了之后就需要检查最大线程数。
  3. 进入到最大线程数检查,如果线程池没有达到最大线程数,那么就创建一个新的线程来执行任务,如果已经达到了最大线程数那么就需要使用拒绝策略。

总结

这里最容易混淆的就是达到核心线程数之后的操作。很多人会认为如果当前线程池中的线程数达到了核心线程数之后,后续进入的线程就会直接创建新的线程,然后一直等到达到最大线程数的时候就才会往线程队列中添加待执行任务,一直到队列满了之后,执行拒绝策略,其实不然。根据上面的介绍当核心线程数达到之后,后续进入的线程是先进入到队列中,当队列满了之后才会开启新的线程执行,一直到最大线程之后,才会触发拒绝策略。这一点是需要大家注意的。到这里整个的线程池核心参数介绍就结束了。希望能够帮助到大家。关于线程池的其他面试相关问题,我们会在后续的分享中给大家介绍,希望大家多多关注。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表