6

面试20K的职位必须要熟悉的Java线程池面试题

 2 years ago
source link: https://www.51cto.com/article/718054.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

面试20K的职位必须要熟悉的Java线程池面试题

作者:石杉的架构笔记 2022-09-05 17:49:53
今天跟大家聊一个互联网大厂的Java面试题:使用无界队列的线程池会导致内存飙升吗?
a8ae943311e92e8259362563d0367d42b777de.png

一、背景引入

今天跟大家聊一个互联网大厂的Java面试题:使用无界队列的线程池会导致内存飙升吗?

因为在面互联网大厂的时候,一定会问并发,问并发的时候一定会问到线程池,问到线程池一定会问构造线程池的一些参数的含义。

然后,有一些面试官会就线程池的具体场景,问一些可能会遇到的问题。

所以,在这里就可能有上述那样一个面试中的问题,算是Java面试里相对来说高阶一点的。

我相信大家一定起码知道线程池是个什么东西的。简单来说,就是维护一个池子,池子里面放了很多的线程。

然后来一个任务,某个线程就获取这个任务来执行,任务执行完之后线程是不会释放掉的,而是停留在线程池里继续等待下一个任务。

这样的一个好处是你没必要自己手动频繁的创建和销毁线程,毕竟线程是较重的资源,频繁的创建和销毁对系统性能是没好处的。

我们看看下面的图,回顾一下线程池的含义。

34bbf0000e7b640f4e621101af357eac5d1c77.jpg

二、线程池是如何构造的?

那么平时在Java里写代码的时候,大家记得不记得线程池是如何构造出来的呢?

是不是类似下面那样的代码,比如说我们构造一个线程数量固定的一个线程池:

18c1c82411e0b2ba86e2107c3ec94480a4fbf0.jpg

那么Executors.newFixedThreadPool(10)内部到底又是如何构造出来线程池的呢?

其实很简单,翻开JDK源码就可以看到里面的代码如下:

c5df1ed28733758fae5264d42aa55b13414d4c.jpg

简单来说,就是构造了一个ThreadPoolExecutor对象实例,你大致就认为他是一个线程池吧,传入了一些参数,这些参数大致包含了:

  1. corePoolSize
  2. maximumPoolSize
  3. keepAliveTime
  4. workQueue

假如说我们构造线程池传入的线程数量是10,那么在这里,corePoolSize和maximumSize都是10,keepAliveTime默认就是0,workQueue是一个无界的LinkedBlockingQueue。

接下来,我们具体来看看构造一个线程池传入一些参数之后,具体这个线程池的运行原理是什么。

三、线程池的运行原理

简单来说,刚开始的时候其实线程池里是空的,就是一个线程都没有的,如下图所示。

c5a323981430e15b6e6484a03b623f44bc763f.jpg

接着如果你使用线程池提交一个任务进去,希望由线程池里的一个线程来执行,如下代码所示,就是提交一个任务:

05166a6411ee882cf8554445d5e774e7e656a5.jpg

这个时候,线程池会先看一下,现在池子里的线程数量有没有有达到corePoolSize指定的数量。

现在线程池里的线程数量是0,然后corePoolSize是10,那么肯定没达到了,所以直接会在线程池里创建一个线程出来然后执行这个任务,如下图。

c6fa0f2356405bbe666494e44f72e2cf3fb6d9.jpg

接着假如说,这个线程处理完一个任务了,那么此时线程是不会被销毁的,他会一直等待下一个提交过来的任务。

那么,到底是怎么等待的呢?

很简单,线程池会搭配一个workQueue,比如这里搭配的就是一个无界的LinkedBlockingQueue,几乎可以无限量放入任务。

然后那个线程处理完一个任务之后,就会用阻塞的方式尝试从任务队列里获取任务,如果队列是空的,他就会阻塞卡在那儿不动,直到有人放一个任务到队列里,他才会获取到一个任务然后继续执行,循环往复,如下图。

b5bab2505d60aed7815156219928d7e10bbc1a.jpg

接着再次提交任务,线程池一判断发现,诶?好像线程数量才只有1个,完全比corePoolSize(10个)要小,那么继续直接在池子里创建一个线程,然后处理这个任务,处理完了继续尝试从workQueue里阻塞式获取任务。

一直重复上面的操作,直到线程池里有10个线程了,达到了corePoolSize指定的数量,如下图。

17904f956e73e8e4f3a88978c05b87de3510bd.jpg

这个时候你如果再提交任务,他一下子发现,诶?不对啊,线程池里已经有10个线程了,跟corePoolSize指定的线程数量一样了。

那么现在,我就不需要创建任何一个额外的线程了,现在你只要提交任务,全部直接入队到workQueue里就好。

此时线程池里的线程都阻塞式在workQueue上等待获取任务,有一个任务进来就会唤醒一个线程来处理这个任务,处理完了任务再次阻塞在workQueue上尝试获取下一个任务,如下图所示这个意思。

05dd8fd815d067e1b6c026671f4a0dff8da71d.jpg

这里我们看到他用的是一个无界的LinkedBlockingQueue,但是假如说他用的是一个有界的队列呢?

比如说限定好了队列最多只能放10个任务,那么假如说,线程池里的线程来不及处理任务了,然后队列一下子放满了10个任务。

此时就会出现任务入队的失败,因为队列满了,无法入队。

然后就会尝试再次在线程池里创建线程,这个时候就会一直创建线程直到线程池里的线程数量达到maximumPoolSize指定的数量为止。

虽然这里fixed线程池默认corePoolSize和maximumPoolSize的数量都是一致的,但是可以假设此时maximumPoolSize的数量是20呢?

那么就会继续创建线程,直到线程数量达到20个,然后用额外创建的10个线程在队列满的情况下,继续处理任务。

整个过程,如下图所示:

78be881671e7d8532ad9701cd1c53abad79e60.jpg

接着万一队列满了,然后线程池的线程数量达到了maximumPoolSize指定的数量了,你额外创建线程都无法创建了,此时会如何呢?

答案是:会reject掉,不让你继续提交任务了,此时默认的就是抛出一个异常。

那么,在上图中额外创建出来的,超出corePoolSize的那些线程呢?

他们一旦创建出来之后,会发现线程池数量已经超过corePoolSize了,此时他们会尝试等待workQueue里的任务。

一旦超过keepAliveTime指定的时间,还获取不到任务,比如keepAliveTime是60秒,那么假如超过60秒获取不到任务,他就会自动释放掉了,这个线程就销毁了。

整个过程,如下图所示。

910b956327b9163ec63657216e745dfff3a1fb.jpg

四、无界队列引发的内存飙升

明白了线程池的运行原理了,这个面试题就好解答了。

我们以最常用的fixed线程池举例,他的线程池数量是固定的,因为他用的是近乎于无界的LinkedBlockingQueue,几乎可以无限制的放入任务到队列里。

所以只要线程池里的线程数量达到了corePoolSize指定的数量之后,接下来就维持这个固定数量的线程了。

然后,所有任务都会入队到workQueue里去,线程从workQueue获取任务来处理。

这个队列几乎永远不会满,当然这是几乎,因为LinkedBlockingQueue默认的最大任务数量是Integer.MAX_VALUE,非常大,近乎于可以理解为无限吧。

只要队列不满,就跟maximumPoolSize、keepAliveTime这些没关系了,因为不会创建超过corePoolSize数量的线程的。

同样,给大家来一张图,我们来看看:

f31eca16187701f3d06144211d0c17c29ca86c.jpg

那么此时万一每个线程获取到一个任务之后,他处理的时间特别特别的长,长到了令人发指的地步。比如处理一个任务要几个小时,此时会如何?

当然会出现workQueue里不断的积压越来越多得任务,不停的增加。

这个过程中会导致机器的内存使用不停的飙升,最后也许极端情况下就导致JVM OOM了,系统就挂掉了。

所以这就是这个面试题背后你要知道的线程池的运行原理,以及可能遇到的一些问题,大家要做到心里有数。

责任编辑:姜华 来源: 今日头条

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK