在我们平时的编码中,通常会保存一些对象,这主要是考虑对象创建的成本。比如线程资源,数据库连接资源,或者TCP连接等,这类对象的初始化通常需要很长时间。如果频繁的申请和销毁它们,会消耗大量的系统资源,造成不必要的性能损失。并且这些对象有一个显着的特点,就是可以通过轻量级的重置工作,将它们回收再利用,反复使用。这时候,我们可以使用一个虚拟池来保存这些资源,在我们使用的时候,可以快速的从池中获取一个。在Java中,池化技术被广泛使用。常见的有数据库连接池、线程池等,本文重点介绍连接池和线程池,我们会在后续的博客中介绍。公共池化包CommonsPool2介绍我们先来看一下Java中的公共池化包CommonsPool2,了解对象池的大致结构。根据我们的业务需求,使用这套API可以很方便的实现对象池管理。org.apache.commonscommons-pool22.11.1GenericObjectPool是对象池的核心类。通过传入对象池配置和对象工厂,可以快速创建对象池。publicGenericObjectPool(finalPooledObjectFactoryfactory,finalGenericObjectPoolConfigconfig)案例Redis的一个普通客户端Jedis使用CommonsPool来管理连接池,可以说是一种最佳实践。下图是Jedis使用工厂创建对象的主要代码块。对象工厂类的主要方法是makeObject,它的返回值是PooledObject类型,使用newDefaultPooledObject<>(obj)可以简单的包装返回对象。redis.clients.jedis.JedisFactory,使用工厂创建对象。@OverridepublicPooledObjectmakeObject()throwsException{Jedisjedis=null;尝试{jedis=newJedis(jedisSocketFactory,clientConfig);//主要耗时操作jedis.connect();//返回包装对象returnnewDefaultPooledObject<>(jedis);}catch(JedisExceptionje){if(jedis!=null){try{jedis.quit();}catch(RuntimeExceptione){logger.warn("ErrorwhileQUIT",e);}试试{jedis.close();}catch(RuntimeExceptione){logger.warn("关闭时出错",e);}}扔我;}}再介绍一下对象的生成过程,如下图所示,对象在获取的时候,会先尝试从对象池中取出一个。如果对象池中没有空闲对象,则使用工厂类提供的方法生成一个新的。publicTborrowObject(finalDurationborrowMaxWaitDuration)throwsException{//这里省略了几行while(p==null){create=false;//首先尝试从池中获取它。p=idleObjects.pollFirst();//调用工厂生成新实例if(p==null){p=create();如果(p!=null){创建=true;}}//这里省略几行}//这里省略几行}对象存在哪里?这个存储的责任由一个名为LinkedBlockingDeque的结构承担,它是一个双向队列。接下来看一下GenericObjectPoolConfig的主要属性://GenericObjectPoolConfig本身的属性privateintmaxTotal=DEFAULT_MAX_TOTAL;privateintmaxIdle=DEFAULT_MAX_IDLE;privateintminIdle=DEFAULT_MIN_IDLE;//其父类BaseObjectPoolConfig的属性privatebooleanlifo=DEFAULT_LIFO;privatebooleanfairness=DEFAULT_FAIRNESS;privatelongmaxWaitMillis=DEFAULT_MAX_WAIT_MILLIS;privatelongminEvictableIdleTimeMillis=DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;privatelongevictorShutdownTimeoutMillis=DEFAULT_EVICTOR_SHUTDOWN_TIMEOUT_MILLIS;privatelongsoftMinEvictableIdleTimeMillis=DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;privateintnumTestsPerEvictionRun=DEFAULT_NUM_TESTS_PER_EVICTION_RUN;privateEvictionPolicyevictionPolicy=null;//只有2.6.0应用程序设置这个privateStringevictionPolicyClassName=DEFAULT_EVICTION_POLICY_CLASS_NAME;privatebooleantestOnCreate=DEFAULT_TEST_ON_CREATE;privatebooleantestOnBorrow=DEFAULT_TEST_ON_BORROW;privatebooleantestOnReturn=DEFAULT_TEST_ON_RETURN;privatebooleantestWhileIdle=DEFAULT_TEST_WHILE_IDLE;privatelongtimeBetweenEvictionRunsMillis=DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;privatebooleanblockWhenExhausted=DEFAULT_BLOCK_WHEN_EXHAUSTED;参数很多,要想了解参数的意义,我们首先来看一下一个池化对象在整个池子中的生命周期如下如图所示,pool主要有两个操作:一个是业务线程,一个是检测线程。初始化对象池时,需要指定三个主要参数:maxTotal,对象池管理对象的上限,maxIdle,最大空闲数,minIdle,最小空闲数。其中,maxTotal与业务线程有关。当业务线程要获取对象时,首先会检测是否有空闲对象。如果有一个,返回一个;否则,进入创建逻辑。此时,如果池中的数量已经达到最大值,则创建失败,返回一个空对象。在获取对象的时候,有一个很重要的参数,就是最大等待时间(maxWaitMillis)。这个参数对应用端的性能影响比较大。该参数默认为-1,这意味着在对象空闲之前永远不会超时。如下图,如果对象创建很慢或者使用很忙,业务线程会持续阻塞(blockWhenExhausted默认为true),会导致正常服务无法运行。面试题一般面试官会问:你会把timeout参数设置多大?我通常将最长等待时间设置为接口可以容忍的最大延迟。比如正常服务的响应时间是10ms左右,到了1秒就会有卡顿的感觉,所以这个参数可以设置为500~1000ms。超时后会抛出NoSuchElementException,请求很快失败,不会影响其他业务线程。这种FailFast的想法在Internet上被广泛使用。带有evcit字样的参数主要处理对象驱逐。除了初始化和销毁??的代价高昂之外,池化对象还在运行时占用系统资源。比如连接池会占用多个连接,线程池会增加调度开销。在突发流量情况下,业务会申请超出正常情况的对象资源,并放入池中。当这些对象不再被使用时,我们需要清理它们。超过minEvictableIdleTimeMillis参数指定值的对象将被强制回收。该值默认为30分钟;softMinEvictableIdleTimeMillis参数类似,但是只有当前对象数大于minIdle时才会移除,所以前者的动作更猛烈一些。还有4个测试参数:testOnCreate、testOnBorrow、testOnReturn、testWhileIdle,分别指定在创建、获取、返回和空闲检测时是否检查池化对象的有效性。开启这些检查可以保证资源的可用性,但是会消耗性能,所以默认是false。在生产环境中,建议只将testWhileIdle设置为true,并调整空闲检测间隔时间(timeBetweenEvictionRunsMillis),比如1分钟,以保证资源可用性和效率。JMH测试使用连接池和不使用连接池的性能差距有多大?下面是一个简单的JMH测试例子(见仓库),进行简单的set操作,为redis的key设置一个随机值。@Fork(2)@State(Scope.Benchmark)@Warmup(iterations=5,time=1)@Measurement(iterations=5,time=1)@BenchmarkMode(Mode.Throughput)publicclassJedisPoolVSJedisBenchmark{JedisPool池=newJedisPool(“本地主机”,6379);@BenchmarkpublicvoidtestPool(){Jedisjedis=pool.getResource();jedis.set("a",UUID.randomUUID().toString());绝地武士关闭();}@BenchmarkpublicvoidtestJedis(){Jedisjedis=newJedis("localhost",6379);jedis.set("a",UUID.randomUUID().toString());绝地武士关闭();}//这里省略几行}使用元图表绘制测试结果,显示结果如下图所示。可以看到使用了连接池方式,它的吞吐量是不使用连接池方式的5倍!数据库连接池HikariCPHikariCP来自日文“light”,意为光,意思是软件运行速度快如光速。是SpringBoot默认的数据库连接池。数据库是我们工作中经常用到的一个组件。有很多为数据库设计的客户端连接池。它的设计原理与我们在本文开头提到的基本相同,可以有效减少创建和销毁数据库连接的资源。消耗。同一个连接池有不同的性能。下图是HikariCP的官方测试图,可以看到其优秀的表现。JMH官方测试代码见Github。一般的面试题是这样的:为什么HikariCP快?主要有三个方面:使用FastList代替ArrayList,通过初始化默认值减少越界检查的操作;优化和简化字节码,通过使用Javassist减少动态代理的性能损失,例如使用invokestatic指令替换invokevirtual指令;实现了无锁的ConcurrentBag,减少并发场景下的锁竞争。HikariCP的一些性能优化操作非常值得我们参考。在后面的博客中,我们将详细分析几种优化场景。数据库连接池也面临最大值(maximumPoolSize)和最小值(minimumIdle)的问题。这里还有一个非常高频的面试题:你一般把连接池设置成多大?很多同学认为连接池越大越好。有的同学甚至把这个值设置成1000以上,这是一种误解。根据经验,只要20-50个数据库连接就够了。具体大小要根据业务属性进行调整,但太大肯定是不合适的。HikariCP官方不建议设置minimumIdle的值,默认会设置成和maximumPoolSize一样大。如果你的数据库服务器端连接资源比较空闲,不妨去掉连接池的动态调整功能。另外,根据数据库查询和事务的类型,可以在一个应用中配置多个数据库连接池。很少有人知道这种优化技术。我在这里简单描述一下。通常有两种业务:一种是要求响应时间快,尽快将数据返回给用户;另一个可以在后台慢慢执行,耗时较长,对时效性要求不高。如果这两类业务共用一个数据库连接池,很容易发生资源竞争,进而影响接口的响应速度。虽然微服务可以解决这种情况,但是大部分服务都不具备这个条件,此时可以拆分连接池。如图所示,在同一个业务中,根据业务的属性,我们划分了两个连接池来应对这种情况。HikariCP还提到了另一个知识点,在JDBC4协议中,可以通过Connection.isValid()来检测连接的有效性。这样我们就不需要设置很多测试参数,HikariCP也没有提供这样的参数。于是,缓存池就来了,你可能会发现池(Pool)和缓存(Cache)有很多相似之处。它们之间的一个共同点是对象在相对高速的区域中被处理和存储。我习惯性的把缓存看作数据对象,把池中的对象看作执行对象。缓存中的数据存在命中率问题,而池中的对象一般都是对等的。考虑如下场景,jsp提供了网页的动态功能,执行后可以编译成class文件,加快执行速度;或者,一些媒体平台会定期将热门文章转成静态html页面,只有通过nginx的负载均衡才能处理高并发请求(动静分离)。在这些时候,您很难判断这是针对对象的缓存优化还是池化优化。本质上,它们只是保存了某个执行步骤的结果,这样下次访问它们就不需要从头再来。我通常将这种技术称为结果缓存池,它是各种优化方法的组合。总结我简单总结一下本文的重点:我们从Java中最常见的公共池化包CommonsPool2入手,介绍它的一些实现细节,并讲解一些重要参数的应用;Jedis是在CommonsPool2的基础上进行封装的,通过JMH测试,我们发现经过对象池化后,有近5倍的性能提升;接下来介绍数据库连接池中的快速HikariCP。此外,通过编码技能进一步提高了性能。HikariCP是我关注的类库之一,我也建议您将它添加到您的待办事项列表中。一般来说,当遇到以下场景时,可以考虑使用池化来提高系统性能:对象的创建或销毁需要大量的系统资源;对象的创建或销毁耗时长,操作复杂,等待时间长;创建对象后,可以通过一些状态重置来重复使用它。池化对象后,仅启用优化的第一步。为了达到最佳性能,必须调整池的一些关键参数。合理的池大小加上合理的超时时间可以让池发挥更大的价值。与缓存命中率类似,池的监控也很重要。如下图,可以看到数据库连接池中的连接数长期处于高位没有被释放,等待线程数急剧增加,可以帮助我们快速定位事务数据库的问题。在正常的编码中,类似的场景还有很多。比如Http连接池,Okhttp和Httpclient都提供了连接池的概念,可以类推分析,重点也是连接大小和超时时间;在底层中间件,比如RPC,通常会使用连接池技术加速资源获取,比如Dubbo连接池,Feign转httppclient等技术。你会发现不同资源层级的池化设计是相似的。比如线程池在第二层使用队列来缓冲任务,并提供各种拒绝策略。我们将在后续文章中介绍线程池。线程池的这些特点在连接池技术中也可以借鉴,缓解请求溢出,创建一些溢出策略。实际上,我们也是这样做的。那怎么办呢?有哪些做法?这部分留给大家思考。