1。背景介绍今天跟大家聊聊一个互联网大公司的Java面试题:使用无界队列的线程池会不会导致内存飙升?因为当你面对一个大型的互联网公司时,你肯定会问并发的问题。问到并发,肯定会问到线程池。当你问起线程池的时候,肯定会问到构造线程池的一些参数的含义。然后,有些面试官会问一些关于线程池具体场景的可能问题。所以可能会有像上面这样的面试题,在Java面试中属于比较高级的。相信大家至少应该知道什么是线程池吧。简单点说就是维护一个池子,池子里有很多线程。然后一个任务来了,某个线程获取这个任务去执行。任务执行完后,线程不会被释放,而是留在线程池中,继续等待下一个任务。这样做的一个好处是您不需要频繁地手动创建和销毁线程。线程毕竟是重资源,频繁的创建和销毁对系统性能不利。我们通过下图来复习一下线程池的含义。2、线程池是如何构造的?那么平时写Java代码的时候,还记得线程池是怎么构造的吗?是不是类似于下面的代码,比如我们构造一个线程数固定的线程池:那么Executors.newFixedThreadPool(10)是如何构造线程池的呢?其实很简单。打开JDK源码可以看到里面的代码如下:简单来说就是构造了一个ThreadPoolExecutor对象实例。你粗略的认为是一个线程池,传入了一些参数,这些参数大致包括已经有:corePoolSizemaximumPoolSizekeepAliveTimeworkQueue如果我们构造线程池时传入的线程数是10,那么这里corePoolSize和maximumSize都是10,keepAliveTime默认为0,workQueue是一个无界的LinkedBlockingQueue。下面我们在构造线程池并传入一些参数后,看看线程池的具体运行原理。3、线程池的运行原理简单来说,线程池一开始其实是空的,也就是没有线程,如下图所示。那么如果你使用线程池提交任务,你想让它由线程池中的一个线程来执行,如下代码所示,就是提交任务:这个时候,线程池会先检查线程池中是否有达到指定数量的corePoolSize。现在线程池的线程数为0,corePoolSize为10,肯定达不到,所以会直接在线程池中创建一个线程,然后执行这个任务,如下图。那么如果说这个线程处理完了一个任务,那么这个时候线程就不会被销毁,它会一直等待下一个提交的任务。那么还等什么呢?很简单,线程池会配一个workQueue,比如这里是一个无界的LinkedBlockingQueue,可以放几乎无限的任务。然后当那个线程处理完一个任务后,它会尝试以阻塞的方式从任务队列中获取任务。如果队列为空,它将阻塞并停留在那里,直到有人将任务放入队列中。会获取到一个任务,然后继续执行,如此循环往复,如下图所示。然后再提交任务,线程池判断发现,嗯?好像线程数只有1,完全小于corePoolSize(10),所以继续直接在池中创建线程,然后处理这个任务。处理完后,继续尝试从workQueue中阻塞任务。重复以上操作,直到线程池中有10个线程,达到corePoolSize指定的数量,如下图所示。如果这个时候你再提交任务,他会一下子找到,嗯?不是,线程池里已经有10个线程了,和corePoolSize指定的线程数一样。所以现在,我不需要创建任何额外的线程。现在您只需要提交任务并将它们全部直接排入workQueue。此时线程池中的线程都阻塞在workQueue上等待获取任务。当有任务进来时,就会唤醒一个线程来处理这个任务。任务处理完成后,会再次阻塞在workQueue上,尝试获取下一个任务,如下图所示。意思是。这里我们看到他使用的是无界LinkedBlockingQueue,但是如果他使用的是有界队列呢?例如,如果队列最多只能有10个任务,那么如果线程池中的线程来不及处理任务,那么队列会一下子充满10个任务。这时候任务会加入队伍失败,因为队列已满,无法加入。然后它会再次尝试在线程池中创建线程。这时候会一直创建线程,直到线程池中的线程数达到maximumPoolSize指定的数量。虽然这里的固定线程池默认有相同数量的corePoolSize和maximumPoolSize,但是可以假设此时maximumPoolSize的数量是20?然后会继续创建线程,直到线程数达到20,等队列满了再使用额外创建的10个线程继续处理任务。整个过程如下图所示:如果队列满了,线程池中的线程数达到了maximumPoolSize指定的数量,不能再创建线程,这时候会发生什么?答案是:会被拒绝,不允许你继续提交任务。这个时候默认是抛出异常。那么,上图中额外创建的超过corePoolSize的线程怎么办?一旦创建,就会发现线程池的数量已经超过了corePoolSize,就会尝试去等待workQueue中的任务。一旦超过keepAliveTime指定的时间,就无法获取任务。比如keepAliveTime为60秒,那么如果超过60秒无法获取到任务,就会自动释放,同时销毁线程。整个过程如下图所示。4、无界队列引起的内存暴涨了解线程池的运行原理,这道面试题很容易回答。我们以最常用的固定线程池为例。线程池的数量是固定的,因为它使用了一个近乎无界的LinkedBlockingQueue,可以几乎无限制地把任务放入队列。所以只要线程池中的线程数达到了corePoolSize指定的数量,那么这个固定的线程数就会一直保持下去。然后,所有任务都会入队到workQueue中,线程会从workQueue中获取任务进行处理。这个队列几乎永远不会满,当然是几乎,因为LinkedBlockingQueue默认的最大任务数是Integer.MAX_VALUE,很大,几乎可以理解为无穷大。只要队列未满,就与maximumPoolSize和keepAliveTime无关,因为不会创建超过corePoolSize个数的线程。同样的,给大家放一张图,大家看一下:那么万一这时候每个线程都获取到一个task,那么处理的时间就非常长,长的离谱。例如,如果处理一个任务需要几个小时,这时候会发生什么?当然,workQueue中的任务会越来越多,还会不断增加。这个过程会导致机器内存占用飙升,极端情况下,可能会导致JVMOOM,系统挂掉。那么这就是这道面试题背后你需要知道的线程池的运行原理,以及你可能会遇到的一些问题。每个人都应该意识到这一点。
