Java岗大厂面试百日冲刺【Day37】—— 并发编程3
本文已获得原作者 _陈哈哈 授权并经过重新整理规划后发布。
本栏目Java开发岗高频面试题主要出自以下各技术栈:Java基础知识、集合容器、并发编程、JVM、Spring全家桶、MyBatis等ORMapping框架、MySQL数据库、Redis缓存、RabbitMQ消息队列、Linux操作技巧等。
面试题1:你了解线程池么?简单介绍一下。
java提供的一个java.util.concurrent.Executor接口的实现用于创建线程池。
线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
假设一个服务器完成一项任务,创建线程时间T1 ,在线程中执行任务的时间T2,销毁线程时间为T3。如果:T1 + T3 远大于 T2,则可以采用线程池,大大缩短T1、T3时间,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
当线程池中有任务需要执行时,线程池会判断如果池里的线程数没有占满就会新建线程池进行任务执行,如果线程池中的线程数量已经超过核心线程数,这时候任务就会被放入任务队列中排队等待执行;如果任务队列也满了,并且线程池没有达到最大线程数,就会新建非核心线程来执行任务;如果超过了最大线程数,就会执行饱和策略(拒绝执行)。
追问1:连接池 和 线程池是一个意思么?有什么区别?
不同点
连接池:
- 连接池是面向数据库连接的
- 连接池是为了优化数据库连接资源
- 连接池有点类似在客户端做优化
数据库连接是一项有限的昂贵资源,一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的性能低下。
数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并将这些连接组成一个连接池,由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。
线程池:
- 线程池是面向后台程序的
- 线程池是是为了提高内存和CPU效率
- 线程池有点类似于在服务端做优化
线程池是一次性创建一定数量的线程(应该可以配置初始线程数量的),当用请求过来不用去创建新的线程,直接使用已创建的线程,使用后又放回到线程池中。
避免了频繁创建线程,及销毁线程的系统开销,提高是内存和CPU效率。
相同点
都是事先准备好资源,避免频繁创建和销毁的代价。
面试题2:线程池中核心线程数量大小你是怎么设置的?
分析一般从几个角度考虑:
- 任务的性质:CPU密集型的任务、IO密集型任务、混合型任务。
- 任务的优先级:高、中、低
- 任务执行时间:长、中、短
- 任务的依赖性:是否依赖其它系统资源,如数据库的连接等。
cpu密集型
尽量减少线程数;比如像加解密,压缩、计算等一系列需要大量耗费 CPU 资源的任务,大部分场景下都是纯 CPU 计算。尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型
任务尽量加大线程数,因为io不占用cpu的资源。比如像 MySQL 数据库、文件的读写、网络通信等任务,这类任务不会特别消耗 CPU 资源,但是 IO 操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为CPU核心数 * 2。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型
尽量根据实际情况进行拆分,根据运行时间来决定。
可见,线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;
追问1:核心线程数量过大或过小会造成什么后果?
当线程池中核心线程数量过大时,线程与线程之间会争取CPU资源,这样就会导致上下文切换。过多的上下文切换会增加线程的执行时间,影响了整体执行的效率;
多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效的执行,CPU采取的策略是为了每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让其他线程使用,这个过程就属于一次上下文切换。
当线程池中的核心线程数量过少时,如果统一时间有大量任务需要处理,可能会导致大量任务在任务队列中排队等待执行,甚至会出现队列满了之后任务无法执行的情况,或者大量任务堆积在任务队列导致内存溢出(OOM)。
面试题3:线程池都有哪些状态呀?
RUNNING
:线程池的初始化状态是RUNNING,能够接收新任务,以及对已添加的任务进行处理。
SHUTDOWN
:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。 调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
STOP
:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。 调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
TIDYING
:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
TERMINATED
:线程池彻底终止,就变成TERMINATED状态。线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
追问1:什么条件下会进入TERMINATED状态
- 线程池不是RUNNING状态;
- 线程池状态不是TIDYING状态或TERMINATED状态;
- 如果线程池状态是SHUTDOWN并且workerQueue为空;
- workerCount为0;
- 设置TIDYING状态成功。