线程池
如何创建线程池
JDK中提供了创建线程池的类,大家首先想到的一定是Executors类,没错,可以通过Executors类来创建线程池,但是不推荐(原因后面会分析)。
在面试过程中经常会被问到请说说如何创建线程池,线程池的参数有哪些等等。 以下我们都会一一解答
Executors类只是个静态工厂,提供创建线程池的几个静态方法(内部屏蔽了线程池参数配置细节),而真正的线程池类是ThreadPoolExecutor。ThreadPoolExecutor构造方法如下:
参数解释
-
corePoolSize:核心线程数。如果等于0,则任务执行完后,没有任务请求进入时销毁线程池中的线程。如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。设置过大会浪费系统资源,设置过小导致线程频繁创建。
-
maximumPoolSize:最大线程数。必须大于等于1,且大于等于corePoolSize。如果与corePoolSize相等,则线程池大小固定。如果大于corePoolSize,则最多创建maximumPoolSize个线程执行任务
-
keepAliveTime:线程空闲时间。线程池中线程空闲时间达到keepAliveTime值时,线程会被销毁,只到剩下corePoolSize个线程为止。默认情况下,线程池的最大线程数大于corePoolSize时,keepAliveTime才会起作用。如果allowCoreThreadTimeOut被设置为true,即使线程池的最大线程数等于corePoolSize,keepAliveTime也会起作用(回收超时的核心线程)。
-
unit:TimeUnit表示时间单位。
-
workQueue:缓存队列。当请求线程数大于corePoolSize时,线程进入BlockingQueue阻塞队列。
-
threadFactory:线程工厂。用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。
-
handler:执行拒绝策略对象。当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略,可以看作简单的限流保护。
以上解释可能会看不懂,没关系我们举一个列子画图来解释以上参数:
我们来举一个列子,银行,我们去银行办理业务,一个人比作一个线程,银行比作是线程池,银行的一个个受理窗口比作是线程池的线程数,序号比作是线程池的第几个参数。
平时比如周1-周5人比较少 银行上班的人员就比较少 所以银行一般只有2个受理窗口 最多只有5个受理窗口 所以比作线程池的最大线程数就是5,然后核心线程数就是2,所以345窗口是默认关闭的,只有2个窗口打开,这个时候我们人(线程)进入银行(线程池)办理业务 ,进去之后看见受理窗口没有满就直接去受理窗口(核心线程数)。
如下图:
这个时候核心线程数满了,如果门口还有人(线程)进来,会进入候客区(阻塞队列),如下图:
如果这个时候是人来的比较多(业务高峰期,线程数比较多) ,这个时候阻塞队列也满了。
如果再进人的话候客区也没有座位(阻塞队列已经满了放不下了),这个时候会顾客会投诉了,明明有这么多受理窗口却没开启窗口(线程池的线程数),这个时候经理会让一些工作人员回来上班,受理窗口会慢慢打开,如果核心线程数满了 阻塞队列也满了,再进线程的话,如果最大线程数没满,那么会打开线程数的窗口。线程池采用什么方式创建窗口那么就是第6个参数threadFactory
之后窗口慢慢打开打开到最大线程数就不能再打开了,这个时候最大受理窗口(最大线程数)满了,候客区(阻塞队列也满了),这个时候不能再进来人了,再进来人也处理不了这么多人的业务了。如图
这个时候银行(线程池)已经达到饱和 ,再进人银行就要挤满了,这个时候如果还是有人要进来就会出问题了,这个时候就要采取我们线程池第7个参数了,拒绝策略
具体的拒绝策略后续会讲到。
这个时候高峰期过去了,线程的人数也即将减少。如图
这个时候临时加班的那3个工作人员眼看也没有人来了,还在加班,这个时候经理就会说,如果你们这个窗口在5分钟之内的窗口都没有人来受理,那么你们就可以下班回家了。这个时候就是我们线程池的第3个和第4个参数,keepAliveTime和unit:TimeUnit,如果我们线程池除了核心线程数,也就是除了正常工作人员(前2个窗口),只要扩充的窗口在多少分钟之内没有人来受理就会关闭窗口。参数3,线程空闲时间,参数4,线程空间时间的单位
线程池相关类结构
ExecutorService接口继承了Executor接口,定义了管理线程任务的方法。
ExecutorService的抽象类AbstractExecutorService提供了submit、invokeAll()等部分方法实现,但是核心方法Executor.execute()并没有实现。
因为所有任务都在这个方法里执行,不同的线程池实现策略会有不同,所以交由具体的线程池来实现。
线程池种类
-
newSingleThreadExecutor:创建单线程的线程池,核心线程数和最大线程数都为1,相当于串行执行。
-
newFixedThreadPool:创建固定线程数的线程池。核心线程数等于最大线程数,不存在空闲线程,keepAliveTime为0。
-
newCachedThreadPool:核心线程数为0,最大线程数为Integer.MAX_VALUE,是一个高度可伸缩的线程池。存在OOM风险。keepAliveTime为60,工作线程处于空闲状态超过keepAliveTime会回收线程。
禁止直接使用Executors创建线程池原因:
Executors.newCachedThreadPool和Executors.newScheduledThreadPool两个方法最大线程数为Integer.MAX_VALUE,最大线程数可以达到21亿多,创建这么多线程,肯定会抛出OOM异常。
Executors.newSingleThreadExecutor和Executors.newFixedThreadPool两个方法的workQueue参数为new LinkedBlockingQueue(),容量为Integer.MAX_VALUE,阻塞队列的容量最大是21亿,会堆积大量的请求,如果瞬间请求非常大,会有OOM风险。
如果线程池满了怎么办
会执行线程拒绝策略
ThreadPoolExecutor提供了四个公开的内部静态类:
-
AbortPolicy:默认,丢弃任务并抛出RejectedExecutionException异常。
-
DiscardPolicy:丢弃任务,但是不抛出异常(不推荐)。
-
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中。
-
CallerRunsPolicy:调用任务的run()方法绕过线程池直接执行。
友好的拒绝策略:
保存到数据库进行削峰填谷。在空闲时再提出来执行。
转向某个提示页面
打印日志
为什么要用线程池?
池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。