Java线程池实现原理及其在美团业务中的实践
Java线程池实现原理及其在美团业务中的实践
⼀、写在前⾯
1.1 线程池是什么
线程池(Thread Pool)是⼀种基于池化思想管理线程的⼯具,经常出现在多线程服务器中,如MySQL。
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执⾏的任务。这种做法,⼀⽅⾯避免了处理任务时创建销毁线程开销的代价,另⼀⽅⾯避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利⽤。
⽽本⽂描述线程池是JDK中提供的ThreadPoolExecutor类。
当然,使⽤线程池可以带来⼀系列好处:
降低资源消耗:通过池化技术重复利⽤已创建的线程,降低线程创建和销毁造成的损耗。
提⾼响应速度:任务到达时,⽆需等待线程创建即可⽴即执⾏。
提⾼线程的可管理性:线程是稀缺资源,如果⽆限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使⽤线程池可以进⾏统⼀的分配、调优和监控。
提供更多更强⼤的功能:线程池具备可拓展性,允许开发⼈员向其中增加更多的功能。⽐如延时定时线程池
ScheduledThreadPoolExecutor,就允许任务延期执⾏或定期执⾏。
1.2 线程池解决的问题是什么
线程池解决的核⼼问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执⾏,有多少资源需要投⼊。这种不确定性将带来以下若⼲问题:
1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会⾮常巨⼤。
2. 对资源⽆限申请缺少抑制⼿段,易引发系统资源耗尽的风险。
3. 系统⽆法合理管理内部的资源分布,会降低系统的稳定性。
为解决资源分配这个问题,线程池采⽤了“池化”(Pooling)思想。池化,顾名思义,是为了最⼤化收益并最⼩化风险,⽽将资源统⼀在⼀起管理的⼀种思想。
Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia
“池化”思想不仅仅能应⽤在计算机领域,在⾦融、设备、⼈员管理、⼯作管理等领域也有相关的应⽤。
在计算机领域中的表现为:统⼀管理IT资源,包括服务器、存储、和⽹络资源等等。通过共享资源,使⽤户在低投⼊中获益。除去线程池,还有其他⽐较典型的⼏种使⽤策略包括:
1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎⽚。
2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
3. 实例池(Object Pooling):循环使⽤对象,减少资源在初始化和释放时的昂贵损耗。
在了解完“是什么”和“为什么”之后,下⾯我们来⼀起深⼊⼀下线程池的内部实现原理。
⼆、线程池核⼼设计与实现
在前⽂中,我们了解到:线程池是⼀种通过“池化”思想,帮助我们管理线程⽽获取并发性的⼯具,在Java中的体现是ThreadPoolExecutor类。那么它的的详细设计与实现是什么样的呢?我们会在本章进⾏详细介绍。
2.1 总体设计
Java中的线程池核⼼实现类是ThreadPoolExecutor,本章基于JDK 1.8的源码来分析Java线程池的核⼼设计与实现。我们⾸先来看⼀下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。
ThreadPoolExecutor实现的顶层接⼝是Executor,顶层接⼝Executor提供了⼀种思想:将任务提交和任务执⾏进⾏解耦。⽤户⽆需关注如何创建线程,如何调度线程来执⾏任务,⽤户只需提供Runnable对象,将任务的运⾏逻辑提交到执⾏器(Executor)中,由Executor框架完成线程的调配和任务的执⾏部分。ExecutorService接⼝增加了⼀些能⼒:(1)扩充执⾏任务的能⼒,补充可以为⼀个或⼀批异步任务⽣成Future的⽅法;(2)提供了管控线程池的⽅法,⽐如停⽌线程池的运⾏。AbstractExecutorService则是上层的抽象类,将执⾏任务的流程串联了起来,保证下层的实现只需关注⼀个执⾏任务的⽅法即可。最下层的实现类ThreadPoolExecutor实现最复杂的运⾏部
java线程池创建的四种分,ThreadPoolExecutor将会⼀⽅⾯维护⾃⾝的⽣命周期,另⼀⽅⾯同时管理线程和任务,使两者良好的结合从⽽执⾏并⾏任务。ThreadPoolExecutor是如何运⾏,如何同时维护线程和执⾏任务的呢?其运⾏机制如下图所⽰:
线程池在内部实际上构建了⼀个⽣产者消费者模型,将线程和任务两者解耦,并不直接关联,从⽽良好的缓冲任务,复⽤线程。线程池的运⾏主要分成两部分:任务管理、线程管理。任务管理部分充当⽣产者的⾓⾊,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执⾏该任务;(2)缓冲到队列中等待线程执⾏;(3)拒绝该任务。线程管理部分是消费者,它们被统⼀维护在线程池内,根据任务请求进⾏线程的分配,当线程执⾏完任务后则会继续获取新的任务去执⾏,最终当线程获取不到任务的时候,线程就会被回收。
接下来,我们会按照以下三个部分去详细讲解线程池运⾏机制:
1. 线程池如何维护⾃⾝状态。
2. 线程池如何管理任务。
3. 线程池如何管理线程。
2.2 ⽣命周期管理
线程池运⾏的状态,并不是⽤户显式设置的,⽽是伴随着线程池的运⾏,由内部来维护。线程池内部使⽤⼀个变量维护两个值:运⾏状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运⾏状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了⼀起,如下代码所⽰:
private final AtomicInteger ctl =new AtomicInteger(ctlOf(RUNNING,0));
ctl这个AtomicInteger类型,是对线程池的运⾏状态和线程池中有效线程的数量进⾏控制的⼀个字段, 它同时包含两部分的信息:线程池的运⾏状态 (runState) 和线程池内有效线程的数量 (workerCount),⾼3位保存runState,低29位保存workerCount,两个变量之间互不⼲扰。⽤⼀个变量去存储两个值,可避免在做相关决策时,出现不⼀致的情况,不必为了维护两者的⼀致,⽽占⽤锁资源。通过阅读线
程池源代码也可以发现,经常出现要同时判断线程池运⾏状态和线程数量的情况。线程池也提供了若⼲⽅法去供⽤户获得线程池当前的运⾏状态、线程个数。这⾥都使⽤的是位运算的⽅式,相⽐于基本运算,速度也会快很多。
关于内部封装的获取⽣命周期状态、获取线程池线程数量的计算⽅法如以下代码所⽰:
private static int runStateOf(int c){return c &~CAPACITY;}//计算当前运⾏状态
private static int workerCountOf(int c){return c & CAPACITY;}//计算当前线程数量
private static int ctlOf(int rs,int wc){return rs | wc;}//通过状态和线程数⽣成ctl
ThreadPoolExecutor的运⾏状态有5种,分别为:
其⽣命周期转换如下⼊所⽰:
2.3 任务执⾏机制
2.3.1 任务调度
任务调度是线程池的主要⼊⼝,当⽤户提交了⼀个任务,接下来这个任务将如何执⾏都是由这个阶段决定的。了解这部分就相当于了解了线程池的核⼼运⾏机制。
⾸先,所有任务的调度都是由execute⽅法完成的,这部分完成的⼯作是:检查现在线程池的运⾏状态、运⾏线程数、运⾏策略,决定接下来执⾏的流程,是直接申请线程执⾏,或是缓冲到队列中执⾏,亦或是直接拒绝该任务。其执⾏过程如下:
1. ⾸先检测线程池运⾏状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执⾏任务。
2. 如果workerCount < corePoolSize,则创建并启动⼀个线程来执⾏新提交的任务。
3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动⼀个线
程来执⾏新提交的任务。
5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理⽅式是直接抛
异常。
其执⾏流程如下图所⽰:
2.3.2 任务缓冲
任务缓冲模块是线程池能够管理任务的核⼼部分。线程池的本质是对任务和线程的管理,⽽做到这⼀点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配⼯作。线程池中是以⽣产者消费者模式,通过⼀个阻塞队列来实现的。阻塞队列缓存任务,⼯作线程从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是⼀个⽀持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为⾮空。当队列满时,存储元素的线程会等待队列可⽤。阻塞队列常⽤于⽣产者和消费者的场景,⽣产者是往队列⾥添加元素的线程,消费者是从队列⾥拿元素的线程。阻塞队列就是⽣产者存放元素的容器,⽽消费者也只从容器⾥拿元素。
下图中展⽰了线程1往阻塞队列中添加元素,⽽线程2从阻塞队列中移除元素:
使⽤不同的队列可以实现不⼀样的任务存取策略。在这⾥,我们可以再介绍下阻塞队列的成员:
2.3.3 任务申请
由上⽂的任务分配部分可知,任务的执⾏有两种可能:⼀种是任务直接由新创建的线程执⾏。另⼀种是线程从任务队列中获取任务然后执⾏,执⾏完任务的空闲线程会再次去从队列中申请任务再去执⾏。第⼀种情况仅出现在线程初始创建的时候,第⼆种是线程获取任务绝⼤多数的情况。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。