专栏首页捡田螺的小男孩面试官:Java线程池了解?如果你还回答不好,那还不赶快收藏!

面试官:Java线程池了解?如果你还回答不好,那还不赶快收藏!

写在前面

本文将根据面试中常被问到的 Java线程池 展开抽丝剥茧的解析,这个问题可以说是百分之百会在Java程序员面试中被问到,因为在工作中这个需求实在是太普遍了。Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

本文成文的思路将根据面试中问答的流程展开,读者完全可以将本文展开的知识点作为回答此问题的常规套路,如果你掌握本文所列出的知识点,那么就因这一个问题就可以让面试官对你刮目相看。

线程池的作用

在被问到,你是否了解线程池时,这个毫无疑问,肯定都了解,那么从哪开始说呢?必须是使用线程的好处,也就是能解决什么问题。

先说出为什么需要使用线程池,也就是背景:

「因为创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁,所以要使用线程池」

然后再说出线程池如果解决上面的问题,这里列举额有三个优点:

  1. 降低资源消耗;
  2. 提高响应速度;
  3. 提高线程的可管理性;

线程池的实现原理

回答完使用线程池的必要性,接着就是重头戏,线程池的实现原理。

开门见山,先介绍线程池的实现类 ThreadPoolExecutor 是如何使用的,包含哪些参数,含义是什么?

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

要创建一个线程池,构造函数是比较复杂的,一共包含7个参数,具体每个参数的含义如下:

  1. corePoolSize: 线程池的基本大小,当线程池中线程的数量没有达到corePoolSize大小时,每提交一个任务到线程池就会创建一个线程来执行任务,即使其他线程空闲也会创建,直到数据等于线程池基本大小,就不再继续创建,而是在下面的逻辑。需要说明的是如果调用了线程池的 prestartAllCoreThreads() 方法,线程池会提前创建并启动所有基本线程。
  2. maximumPoolSize:表示线程池最大可以创建的线程数,当提交的任务特别多时,corePoolSize大小的数量搞不定就得额外加了,但是也不能无限加,只能加到maximumPoolSize 大小。当任务减少不需要这么多线程的时候就会减少,直到corePoolSize大小。
  3. keepAliveTime & unit:这两个参数是表示线程的最大空闲时间和时间单位的,也就是说当线程池中的线程增长到maximumPoolSize大小后,任务减少,线程在空闲keepAliveTime 时间后,如果数量大于corePoolSize就会销毁。
  4. workQueue:工作阻塞队列,表示当线程池中基本大小的线程池都处于运行状态,那么再提交任务就会放到声明的workQueue队列中,可以选择以下几个队列:
    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool() 使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  5. threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
  6. handler:饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
    • AbortPolicy:直接抛出异常,会 throws RejectedExecutionException。
    • CallerRunsPolicy:调用者所在线程自己来运行任务。
    • 丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
    • DiscardPolicy:不处理,丢弃掉。

我们再结合一张图给看看Java线程池的设计实现原理,帮你将上面线程池创建所需的参数串起来:

也就是说当线程池ThreadPoolExecutor执行execute或者submit方法时线程池的处理情况是这样的:

  1. 如果当前线程池中的线程少于 corePoolSize 则直接创建新线程来执行任务,注意这一步需要获取全局锁;
  2. 如果线程池中运行的线程大于等于 corePoolSize ,则将任务加入 workQueue,即阻塞队列。
  3. 如果阻塞队列也满了,任务无法加入,但是当前线程数小于 maximunPoolSize 最大线程数,则创建一个线程来执行任务,这一步骤需要获取全局锁;
  4. 如果当前线程数量已经等于maximunPoolSize,这时提交的任务将会被拒绝,并且调用 RejectedExecutionHandler.rejectedExecution() 方法;

可以看出线程池是一个 「生产者-消费者」 模型。使用线程的一方是生产者,线程池本身是消费者。因为使用方是向线程池中丢任务(Runnable/Callable),而线程池本身消费这些提交的任务。

ThreadPoolExecutor 采取上述的实现原理,尽可能避免了在执行execute,submit方法时获取全局锁(性能瓶颈),因为只要提交的任务数达到 corePoolSize 时,几乎后面所有的 execute、submit 提交任务都是再走步骤2,无需获取锁,设计的是相当牛逼的。

如果向线程池提交任务

在回答了线程池的实现原理后,那么具体如何使用呢?你就可以回答这两种向线程池提交任务的方式,以及他们之间的区别和使用场景。

一共有两种方法提交任务:

  • execute()
  • submit()

区别在于,execute 方法用于提交不需要返回值的任务,一方面无法判断任务是否执行成功,也无法获取线程的执行结果。

public void execute(Runnable command)

可以看到 execute接收的是一个 Runnable实例,并且这个方法是没有返回结果的。

那么你肯定会问,很多场景下,我们是需要获取任务的执行结果的。这种情况就可以使用 submit() 方法,ThreadPoolExecutor 提供了下面三个 submit 方法,方法签名如下:

// 提交Runnable任务
Future submit(Runnable task);
// 提交Callable任务 
Future submit(Callable task);
// 提交Runnable任务及结果引用 
Future submit(Runnable task, T result);

submit 方法都会返回 Future 对象,通过 future 对象的 future.isDone 方法我们就可以判断任务是否执行完成,以及通过 get() 和 get(timeout, unit) 获取任务的返回值。这两个get方法都会阻塞当前调用线程直到任务完成;

这里再多说一点,我们在平时使用Future阻塞获取任务结果时,可以使用 FutureTask 这个工具类,他同时实现了Runnable和Future接口,也就是说即可以作用任务传递给线程池,也可以用来获取子线程的执行结果。

示例如下:

// 创建FutureTask
FutureTask futureTask = new FutureTask<>(()->"这是返回结果" );
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交FutureTask 
es.submit(futureTask);
// 获取计算结果
String result = futureTask.get();

关闭线程池

线程池的关闭,我们可以使用线程池的 shutdown 和 shutdownNow 方法。它俩的原理都是遍历线程池中的线程然后逐个调用线程的interrupt方法来中断线程,但是这块仅仅是调用中断方法,并不意味着线程就会终止,前提是线程执行的任务可以响应中断,要不然可能永远无法终止,对 线程中断不熟悉的小伙伴可以看看 七哥这篇踩坑指南:一个线程中断引发Bug的“爆肝”排查经历

但上面这两个方法还是存在一定差异的,即shutdownNow方法调用后,首先将线程池的状态设为 STOP,然后调用线程池中所有线程的interrupt方法(包含正在运行的线程),并且返回队列中等待执行任务的列表。而shutdown方法只是将线程状态设为SHUTDOWN状态,然后调用线程池中空闲线程的interrupt方法。

线程池的参数如何配置

当你回答了上面所有线程池的知识点后,一般情况下已经差不多了,不过大厂的面试官还可能会问,既然你知道了原理,那么平时使用的时候,这些参数都是如何配置的呢?

遇到这个问题你的思路得清晰,这个数字肯定不是拍脑袋决定的,不要急于给出数字,而是从场景展开:

要想合理的使用线程池,那么就要首先分析任务特性,是CPU密集型还是IO密集型。

  1. CPU密集型的任务,应该配置尽可能少的线程数,一般配置 CPU个数+1个线程的线程池;
  2. IO密集型任务,因为并不是一直在执行任务,则应分配尽可能多的线程,一般配置2*CPU个数 的线程数量;

如果获取机器的CPU个数,我们可以使用Runtime.getRuntime().availableProcessors();

如果需要处理的任务是有优先级的,则可以使用PriorityBlockingQueue这个阻塞队列作为工作队列,优先级高的先执行。

值得一提的是,使用线程池建议使用有界队列,因为能增加系统的稳定性,我们之前就因为使用了 Executors.newFixedThreadPool() 创建的线程池,其默认使用了无界的 LinkedBlockingQueue 导致在数据库异常时,任务积压,线上频繁FGC,最终内存爆了,整个服务不可用。后来改为有界的工作队列后,就会不断抛出任务抛弃异常,便于监控发现并且不会导致整个服务不可用,只是线程任务异常。

总结

本文基于面试场景,对于Java线程池展开了解析,相信你如果能将本文的内容做到了然于心,以后面试碰到Java线程池这个问题就再也不会垂头丧气,而且胸有成竹。

如果本文内容你觉得有所收获,请帮忙点个「三连」(转发、在看、点赞)

点个 在看

喜欢是一种感觉

在看是一种支持

↘↘↘

本文分享自微信公众号 - 捡田螺的小男孩(gh_873ad5979a0b)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-11-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 个人珍藏的80道多线程并发面试题(11-20答案解析)

    个人珍藏的80道Java多线程/并发经典面试题,现在给出11-20的答案解析哈,并且上传github哈~

    捡田螺的小男孩
  • 给你的Java程序拍个片子吧:jstack命令解析

    如果有一天,你的Java程序长时间停顿,也许是它病了,需要用jstack拍个片子分析分析,才能诊断具体什么病症,是死锁综合征,还是死循环等其他病症,本文我们一起...

    捡田螺的小男孩
  • 源码分析-使用newFixedThreadPool线程池导致的内存飙升问题

    使用无界队列的线程池会导致内存飙升吗?面试官经常会问这个问题,本文将基于源码,去分析newFixedThreadPool线程池导致的内存飙升问题,希望能加深大家...

    捡田螺的小男孩
  • 40 个Java多线程问题总结

    原文地址:http://www.cnblogs.com/xrq730/p/5060921.htm

    一个优秀的废人
  • java中线程池的几种实现方式

    多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力.

    海仔
  • 100道Java并发和多线程基础面试题大集合(含解答),这波面试稳了~

    这些多线程的问题来源于各大网站,可能有些问题网上有、可能有些问题对应的答案也有、也可能有些各位网友也都看过,但是本文写作的重心就是所有的问题都会按照自己的理解回...

    程序员白楠楠
  • 线程的创建

    1. 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。

    黑洞代码
  • 面试必考——线程池原理概述

    线程池的源码解析较为繁琐。各位同学必须先大体上理解线程池的核心原理后,方可进入线程池的源码分析过程。

    黑洞代码
  • 图文介绍进程和线程的区别

    先了解一下操作系统的一些相关概念,大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制...

    趣学程序-shaofeer
  • 笔记09 - 线程池刨根问底

    我们知道CPU运行的最小单位是线程,Java中实现并发是通过多线程来完成的,利用多线程提高了对CPU资源的利用率,但是线程的创建和销毁是很消耗性能的。线程的创建...

    码农帮派

扫码关注云+社区

领取腾讯云代金券