Java线程池详解(⼀):线程池实现原理及使⽤
为什么要使⽤线程池?
在⾯向对象编程中,创建和销毁对象是很费时间的,因为创建⼀个对象要获取内存资源或者其它更多资源。在 Java 中更是如此,虚拟机将试图跟踪每⼀个对象,以便能够在对象销毁后进⾏垃圾回收。所以提⾼服务程序效率的⼀个⼿段就是尽可能减少创建和销毁对象的次数,特别是⼀些很耗资源的对象创建和销毁。
如何利⽤已有对象来服务就是⼀个需要解决的关键问题,其实这就是⼀些 "池化资源" 技术产⽣的原因。⽐如数据库连接池,它在很⼤程度上减少了连接建⽴和释放带来的性能开销!
具体的来讲,我们来考虑这样⼀种场景,对于⼀个 web 项⽬,每次过来⼀个请求,都要在服务端创建⼀个新线程来处理请求,请求处理完成后销毁线程,我们将其简单的量化⼀下:
创建线程耗时:T1
处理请求耗时:T2
销毁线程耗时:T3
那么对于处理这样的⼀个请求,总耗时便是 T = T1 + T2 + T3,那我们来想⼀想如果这个请求⾮常简单呢?(事实上,现在很多 web 接受的都是这样的短⼩且⼤量的请求!),T2 耗时很短,则 T1 + T3 > T2,这样使⽤多线程技术不但没有提⾼ CPU 的吞吐量,反⽽降低了。
可以看出 T1,T3 是多线程本⾝的带来的开销,我们渴望减少 T1,T3 所⽤的时间,从⽽减少总耗时 T 的时间。但⼀些线程的使⽤者并没有注意到这⼀点,所以在程序中频繁的创建或销毁线程,这导致 T1 和 T3 在 T 中占有相当⽐例。显然这是突出了线程的弱点(T1,T3),⽽不是优点(并发性)。
线程池技术的提出,正是为了解决上述的问题!它与数据库连接池是同样的道理,使⽤线程池技术有如下⼏个优点:降低资源消耗: 通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗
提⾼响应速度: 当任务到达时,任务可以不需要等到线程创建就能⽴即执⾏
提⾼线程的可管理性: 线程是稀缺资源,如果⽆限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀分配、调优和监控。
线程池的实现原理
接下来我们来看⼀下,当向线程池提交⼀个任务之后,线程池是如何处理这个任务的,如下图所⽰
(1)线程池判断核⼼线程池⾥的线程是否都在执⾏任务。如果不是,则创建⼀个新的⼯作线程来执⾏任务。如果核⼼线程池⾥的线程都在执⾏任务,则进⼊下个流程。
(2)线程池判断⼯作队列是否已经满。如果⼯作队列没有满,则将新提交的任务存储在这个⼯作队列⾥。如果⼯作队列满了,则进⼊下个流程。
(3)线程池判断线程池的线程是否都处于⼯作状态。如果没有,则创建⼀个新的⼯作线程来执⾏任务。如果已经满了,则交给饱和策略来处理这个任务
接下来,拿 J.U.C 包中的 ThreadPoolExecutor 执⾏ execute() ⽅法来举例,看⼀下 Java 中的线程池是如何⼯作的(ThreadPoolExecutor 是 Executor 框架中最核⼼的类,是线程池的实现类)
(1)如果当前运⾏的线程少于 corePoolSize,则创建新线程来执⾏任务(注意,执⾏这⼀步骤需要获取全局锁)
(2)如果运⾏的线程等于或多于 corePoolSize,则将任务加⼊ BlockingQueue
(3)如果⽆法将任务加⼊ BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执⾏这⼀步骤需要获取全局锁)
(4)如果创建新线程将使当前运⾏的线程超出 maximumPoolSize,任务将被拒绝,并调⽤
ThreadPoolExecutor 采取上述步骤的总体设计思路,是为了在执⾏ execute() ⽅法时,尽可能地避免获取全局锁(那将会是⼀个严重的可伸缩瓶颈)。在 ThreadPoolExecutor 完成预热之后(当前运⾏的线程数⼤于等于 corePoolSize),⼏乎所有的 execute() ⽅法调⽤都是执⾏步骤(2) ,⽽步骤(2)不需要获取全局锁。
线程池的使⽤
java线程池创建的四种⼀、线程池的创建
其实在 J.U.C 包下已经提供了 Executors 类,它已经封装实现了四种创建线程池的⽅式,它暴露出⼏个简单的⽅法供开发者调⽤。我们可以根据需要,得到我们想要的线程池类型。这样做其实有利有弊,好的是我们不⽤关⼼那么多参数,只需要简单的指定⼀两个参数就可以;不好的是,这样⼀来⼜屏蔽了很多细节,如果有些参数使⽤默认的,⽽开发者⼜不了解原理的情况下,可能会造成 OOM 等问题。
上述这四种线程池,归根结底都是从 ThreadPoolExecutor 的构造函数创建出来的,只不过传⼊的参数不同罢了。
⽐如,CacheThreadPool
⽐如,FixedThreadPool
⽐如,SingleThreadExecutor
⾄于ScheduledThreadPool,虽然它是由 ScheduledThreadPoolExecutor 的构造函数创建出来的。
但我们⼀路往上,发现它是调⽤了⽗类的构造⽅法,⽽且相信你也猜到了,它的⽗类就是 ThreadPoolExecutor
所以,最核⼼的是要搞懂 ThreadPoolExecutor!
很多公司都不建议或者强制不允许直接使⽤ Executors 类提供的⽅法来创建线程池,例如阿⾥巴巴 Java 开发⼿册⾥就明确不允许这样创建线程池,⼀定要通过 ThreadPoolExecutor(xx,xx,xx…) 来明确线程池的运⾏规则,指定更合理的参数。
我们现在来看⼀下 ThreadPoolExecutor 的⼏个参数和它们的意义,先来看⼀下它最完整参数的重载。
⼀共 7 个参数,接下来本⽂将依次详细介绍⼀下
corePoolSize
核⼼线程数,当有任务进来的时候,如果当前线程数还未达到 corePoolSize 个数,则创建核⼼线程,核⼼线程有⼏个特点:当线程数未达到核⼼线程最⼤值的时候,新任务进来,即使有空闲线程,也不会复⽤,仍然新建核⼼线程
核⼼线程⼀般不会被销毁,即使是空闲的状态,但是如果通过⽅法 allowCoreThreadTimeOut(boolean value) 设置为 true 时,超时也同样会被销毁
⽣产环境⾸次初始化的时候,可以调⽤ prestartCoreThread() ⽅法来预先创建所有核⼼线程,避免第⼀次调⽤缓慢maximumPoolSize
除了有核⼼线程外,有些策略是当核⼼线程完全⽆空闲的时候,还会创建⼀些临时的线程来处理任务,maximumPoolSize 就是核⼼线程+ 临时线程的最⼤上限。临时线程有⼀个超时机制,超过了设置的空闲时间没有事⼉⼲,就会被销毁。
keepAliveTime
这个就是上⾯两个参数⾥所提到的超时时间,也就是线程的最⼤空闲时间,默认⽤于⾮核⼼线程,通过
allowCoreThreadTimeOut(boolean value) ⽅法设置后,也会⽤于核⼼线程。
unit
这个参数配合上⾯的 keepAliveTime ,指定超时的时间单位,秒、分、时等。
workQueue
⾸先应该注意到,它是⼀个 BlockingQueue,即阻塞队列。
它是⼀个等待执⾏的任务队列,如果核⼼线程没有空闲的了,新来的任务就会被放到这个等待队列中。这个参数其实⼀定程度上决定了线程池的运⾏策略,为什么这么说呢,因为队列分为有界队列和⽆界队列。
有界队列: 队列的长度有上限,当核⼼线程满载的时候,新任务进来进⼊队列,当达到上限,如果没有核⼼线程去即时取⾛处理,这个时候,就会创建临时线程。(警惕临时线程⽆限增加的风险)
⽆界队列: 队列没有上限的,当没有核⼼线程空闲的时候,新来的任务可以⽆⽌境的向队列中添加,⽽永远也不会创建临时线程。
(警惕任务队列⽆限堆积的风险)
threadFactory
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论