线程池与连接池

用户在客户端发起Http请求,请求经过网络与Web服务器经过三次握手并建立起连接,服务器接收请求再从线程池中为此次请求分配工作一个工作线程,工作线程开始处理业务逻辑,在处理业务逻辑的过程中,向数据库连接池申请jdbc连接,执行CRUD,执行完毕归还资源,返回请求结果。 这一过程是每个Web开发人员都非常熟悉,每天面对的场景。在这个场景中有两个非常重要的元素,线程池连接池
线程池连接池都是池化资源,他们的概念与作用容易混淆。
线程池管理工作线程,目的是充分利用因IO阻塞而带来的CPU资源。
连接池管理连接,重复利用创建成本较高的连接资源。
连接池是工作在工作线程中的,他们的位置与作用完全不同,参数设置的方法也完全不同。
如下图:

线程池功能

多线程程序可以最大限度的利用多核处理器的计算能力,提高软件的吞吐量和性能,但是如果不对线程加以管理和控制则会造成过多的内存占用,过多CPU的上下文的切换,产生不利影响。所以线程池的作用就是管理线程,基本能力包括:

  • 线程创建(包括设置线程名称,优先级,daemon等属性,限制线程总数)
  • 接收任务,调度工作线程执行任务
  • 监听线程运行时捕获
  • 线程销毁

线程池的工作原理

JDK线程池的实现类是ThreadPoolExecutor,让我们看看源码注解是怎样自我说明的:
ExecutorService使用池化的线程资源来执行任务,它主要解决的问题是对大量异步任务执行的优化并同时对线程,任务资源的管理。
从类的成员变量组成上看ThreadPoolExecutor的主要组成成员有ctl workQueue workers threadFactory rejectedExecutionHandler
ctl: ctl以为Control的缩写,它是一个字段表示两种属性,一个是线程池运行状态,另一个是线程池中有效线程数量。在线程池运行与操作过程中,会检查这两个状态来决定线程池的行为。ThreadPool中提供了计算状态值、线程池线程数量,和ctl值得位运算函数
workers: 工作线程集合,ThreadPoolExecutor将要执行的任务(runnable)封装到Worker中,Worker内部封装了工作线程,和需要被执行的runnable任务。
workQueue: 阻塞队列,用于装载待执行还未执行的任务
threadFactory: 线程创建工厂,它定义了一个newThread()方法,通过这个工厂自定义创建线程,例如设定线程的名称、优先级等属性。例如Tomcat的线程池就使用了TaskThreadFactory来创建线程,它会给线程定义groupName,namePrefix,daemon,优先级几个属性。
RejectedExecutionHandler: 线程任务拒绝handler,在往线程池中提交任务时,如如果无法被接收,会使用rejectHandler拒绝线程。常见拒绝策略有一下四种:

  • Abort 策略在线程池满后,提交新任务时会抛出 RejectedExecutionException,这个也是默认的拒绝策略。
  • Discard 策略会在提交失败时对任务直接进行丢弃。
  • CallerRuns 策略会在提交失败时,由提交任务的线程直接执行提交的任务。
  • DiscardOldest 策略会丢弃最早提交的任务。
    以上介绍了线程池的组成,下面介绍线程池的处理流程:

  • 向线程池中提交任务时,首先会检查当前运行线程数是否大于coreSize,如果不大于则创建一个核心线程来执行任务。
  • 如果大于coreSise,则判断任务队列都是已满,如果没有满则把任务加入到任务队列中等待空闲线程的执行。
  • 如果任务队列已满,则判断当前线程数是否大于maxSize,如果不大于则创建新线程执行任务。
  • 如果已经达到maxSize,则执行拒绝策略。
    值得注意的是,在这个过程中线程池会不断检查线程池状态,如果状态为非Running,那么将不再接收任务,而执行拒绝策略。

线程池有自己的状态:RUNNING SHUTDOWN STOP TIDYING TERMINATED

  • RUNNING: 接收任务并执行或者将任务放入任务队列中,等待执行。
  • SHUTDOWN: 不再接收任何任务,但是会将任务中待执行任务执行完毕。
  • STOP: 不再接收任何任务、不再处理任务队列,将中断所有执行中的任务
  • TIDYING: 所有任务已经终结,工作线程数为0,将回调terminated(),默认terminated()是一个空的实现,用户可以重载此方法来监听TIDYING状态的到达。
  • TERMINATED: terminated() 方法已执行。

线程池的实际使用

讲完了线程池的原理下面我们看看线程池的实际运用场景。
在实际项目运用过程中,用户应该根据不同的场景,来搭配不同的线程池参数,例如Executors工具类就提供了5中不同的线程池创建方法:

  • newFixThreadPool: 特点是线程数固定,无界队列。适用于任务数量不均匀,对内存压力不敏感,单系统负载敏感的场景。
  • Cached:特点使用无容量的队列,不限制最大线程数。适用于要求低延迟,短任务场景。
  • SingleThread: 特点是单线程,无界队列。适用于异步执行,保证顺序的场景。
  • Scheduled: 特点是使用DelayedWorkQueue当任务队列,适用于定期执行任务场景
    下面我们分析一下Tomcat Connector中如何使用线程池的 java public void createExecutor() {
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);
    }
  • 最小线程数(默认): 10
  • 最大线程数(默认): 200
  • 任务队列: TaskQueue,TaskQueue继承自LinkedBlockingQueue,但是重写了offer()poll()等方法。值得关注的是重写了offer()方法,只要没有达到工作线程数没有达到maxSize,都无法将插入成功,这样做的结果是所有请求提交进线程池后都将立即被工作线程执行。直到最大工作线程数。如果达到最大工作线程数,任务将被添加到任务队列中。

连接池功能

我们对数据库进行访问时要使用JDBC,一次JDBC连接的创建需要经过建立连接(包括TCP的三次握手)、认证、授权、资源的初始化和分配等一系列的过程。耗时通常在100ms或更长。通常业务的的CRUD耗时一般在10~50ms。如果频繁的创建连接时间消耗是非常大的,因此我们的优化方案是将连接管理起来,每当一个连接使用完毕后不对其销毁,而是保管起来分配给下一个工作线程重复使用。
连接池对外提供:

  • 获取连接
  • 释放连接
    定义配置参数:
  • 最大连接数
  • 初始连接数
  • 最小空闲连接数
    内部实现:
  • 连接保活
  • 空闲回收
  • 可用性检查

连接池的工作原理

Druid主要是由数组,ReentrantLock和两个信号量empty和notEmpty组成。
数组是存储连接的容器,每一次客户端线程请求连接时,从数组最后一个位置取,创建连接,由专门的创建连接的线程负责,销毁连接由专门的销毁连接的线程负责。
客户端线程,创建连接线程,销毁线程通过lock和两个condition协调下共同协作工作。

连接池的实际使用

连接池参数的设置需要视情况而定,因为不同的业务代码,部署在不同配置的机器中,所需要的参数是不一样的。所以实际的参数设置需要对服务进行压力测试,观察,调优。
连接池的主要作用是连接复用,如果值太小,不够用,会造成工作线程的阻塞,拉低吞吐量。如果值太高,客户端和服务端都会造成过多的维护成本。

maxSize如何设定:
我们需要将系统部署到一个单节点上,然后对其进行压测,测试出系统最大负载是多少。通过压测,不断提高客户端并发数。例如初始客户端并发数为10,依次递增为20,30,40等,在没有到达性能瓶颈前,压测的TPS会随着客户端并发数的增加而增大,响应时间通常会随着客户端并发数增加而增加,但是增加的幅度并不明显。当客户端并发数到达某个阀值的时候,TPS不再增长,反而出现下降,响应时间也出现跳跃式增长,这个时候就是系统最大性能。这个时候就可以得到系统瓶颈时的活跃连接数m(事先将maxSize设置为一个大到应该不会达到的数值)。
为了给连接池流出足够的大小,我们在为期分配一倍的余量,所以maxSize=n*2。
另外,将n当做连接池告警阀值,配置告警。

initialSize如何设定:
连接池初始化大小,一般设定为一个较小的值即可。但是如果线上流量很大,服务上线后,连接不够用,会短时间创建大量线程等待连接创建的问题,可以视情将initalSize设置稍大。
如果是已经在线上运行服务,则可以设置为生产环境平峰的活跃线程数。

minIdle如何设定:
设置为系统在流量低峰时段时活跃连接数

猜你喜欢

转载自www.cnblogs.com/xuerge/p/xian-cheng-chi-yu-lian-jie-chi.html
今日推荐