开足码力,码动人生,本文首发公众号【 Craig无忌 】,关注这个一言不合就开车的的代码界老司机
本文 GitHub上已经收录 https://github.com/BeKingCoding/JavaKing , 一线大厂面试核心知识点、我的联系方式和技术交流群,欢迎Star和完善
前言
昨天在群里有个同学问 Java 并发编程中的线程池内容,本篇文章就给大家介绍下这个在面试中也经常被问到的知识点。
看完后相信你会线程池的原理有更清晰的认识。本文将会从以下几个方面来讲述相关知识,相信大家耐心看了之后肯定有收获,码字不易,别忘了「在看」,「转发」哦。
- 为什么要使用线程池
- 线程池的工作原理
- 线程池的7大核心参数
- 如何正确地使用线程池
正文
**
01 为什么要使用线程池
**
引入一个技术之前,首先应该解答的问题是,这个技术解决什么问题。
在 Java 语言中,创建一个线程看上去非常简单。实现Runnable接口,然后像创建一个对象一样,直接 new Thread 就可以了。
但实际上线程的创建和销毁远不是创建一个对象那么简单。线程的创建需要调用操作系统内核的 API,然后操作系统为其分配一系列资源,所以整个成本很高,导致线程是一个重量级的对象,应该避免频繁创建和销毁。
再来说说线程的上下文切换。
一个 CPU 在一个时刻只能运行一个线程,当其运行一个线程时,由于时间片耗尽或出现阻塞等情况,CPU 会转去执行另外一个线程,这个叫做线程上下文切换。
并且当前线程的任务可能并没有执行完毕,所以在进行切换时需要保存线程的运行状态,以便下次重新切换回来时,能够继续切换之前的状态运行,这个过程就要涉及到用户态和内核态的切换。
什么是用户态和内核态?
当在执行用户自己的代码时,则称其处于用户运行态(用户态),此时处理器特权级最低,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。
当因为系统调用陷入内核代码中执行时,处于内核运行态(内核态),此时处理器处于特权级最高。如果要执行文件操作、网络数据发送等操作必须通过 write、send 等系统调用,这些系统调用会调用内核的代码。会从用户态切换到内核态的内核地址空间去执行内核代码来完成相应的操作,在执行完后又会切换回用户态。
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
为了避免资源过度消耗,所以最好的一种办法就是对线程进行复用,它执行完一个任务,并不需要被销毁,而是让它继续执行其他任务。
线程池是一种线程的使用模式,带来了一系列好处:
(1)避免了线程的重复创建与开销带来的资源消耗代价。
(2)提升了任务响应速度,任务到达时,直接选一个线程执行而无需等待线程的创建。
(3)提高线程的可管理性,线程的统一分配和管理,也方便统一的监控和调优。
这就是线程池最核心的设计思路,复用线程,平摊线程的创建与销毁的开销代价。
02 线程池的工作原理
线程池的工作原理可以简化理解为以下几个步骤:
(1)在线程池的内部,会维护了一个阻塞队列 workQueue 和一组工作线程,工作线程的个数可以在初始化线程池的时候来指定。
(2)用户可以将需要完成的任务提交给线程池,任务会被加入到 workQueue中。
(3)线程池内部维护的工作线程会按照次序,依次消费 workQueue 中的任务并进行执行,在执行结束后并不会销毁。
03 线程池的7大核心参数
我们可以通过 ThreadPoolExecutor 来创建线程池,创建的时候需要指定7大核心参数,每一个参数都代表线程池的特定工作行为,非常重要。
corePoolSize(核心线程数)
将把线程池类比为一个施工队,而线程就是施工队的工人。有些时候比较闲,项目比较少,但是施工队也不能把工人都遣散,需要留下一些核心骨干来以备不时之需,所以至少要留 corePoolSize 个人坚守阵地。
corePoolSize 表示线程池保有的核心线程数,核心线程会一直存活,即使这些线程处于空闲状态没有任务执行,他们也不会被销毁。
maximumPoolSize(最大线程数)
当项目比较多的时候,施工队就需要增加工人,但是也不能无限制地加。最多就加到 maximumPoolSize 个人,当闲下来的时候,施工队就要遣散工人,但是至少保留corePoolSize 个人。
keepAliveTime&unit(存活时间&单位)
上面提到施工队根据忙闲,项目多少来增减工人,那在编程世界里,如何定义忙和闲呢?
很简单,当线程池内部的线程数已经大于 corePoolSize 的时候,一个线程如果在一段时间内,都没有执行任务,说明很闲。
keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit 这么久,那么这个空闲的线程就要被回收了。
workQueue(工作队列)
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
threadFactory(线程工厂)
创建一个新线程时使用的工厂,通过这个工厂可以自定义如何创建线程,例如可以给线程指定一个有意义的名字。
handler(拒绝策略)
如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。
至于拒绝的策略,可以通过 handler 这个参数来指定:
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,直接丢弃任务,抛出RejectedExecutionException。
DiscardPolicy:直接丢弃任务,没有任何异常抛出。
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
04 如何正确地使用线程池
默认的拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
使用线程池,需要注意异常处理的问题。任务在执行的过程中出现运行时异常,会导致执行任务的线程终止,最稳妥和简单的方案还是捕获所有异常并按需处理。
需要注意的一点,在《阿里巴巴Java开发手册》也着重强调,尽可能不要使用Executors 工具类来直接创建线程池,通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
(1)FixedThreadPool 和 SingleThreadPool 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
(2)CachedThreadPool 和 ScheduledThreadPool 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
文末福利
最近各大互联网公司的秋招都陆陆续续开始了,还在找工作的小伙伴可以后台回复关键字进入对应的秋招/内推/面试群,我给大家整理了各大公司的内推通道、简历模板还有历年的笔试题,大家要好好准备哦。还可以帮助大家免费修改简历、模拟面试哦~
关注公众号「Craig无忌」
创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!