最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗8ms左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有0。01ms,这篇文章介绍对象池相关知识。 1。什么是对象池 池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以使用。对象池大多数场景下都是缓存着创建成本过高或者需要重复创建使用的对象,从池子中取出对象的时间是可以预测的,但是新建一个对象的时间是不确定的。 当需要一个新对象时,就向池中借出一个,然后对象池标记当前对象正在使用,使用完毕后归还到对象池,以便再次借出。 常见的使用对象池化的场景:对象创建成本过高。需要频繁地创建大量重复对象,会产生很多内存碎片。同时使用的对象也不会太多。常见的具体场景如数据库连接池、线程池等。2。为什么需要对象池 如果一个对象的创建成本很高,比如建立数据库的连接时耗时过长,在不使用池化技术的情况下,我们的查询过程可能是这样的。查询1:建立数据库连接发起查询收到响应关闭连接查询2:建立数据库连接发起查询收到响应关闭连接查询3:建立数据库连接发起查询收到响应关闭连接 在这种模式下,每次查询都要重新建立关闭连接,因为建立连接是一个耗时的操作,所以这种模式会影响程序的总体性能。 那么使用池化思想是怎么样的呢?同样的过程也会转变成下面的步骤。初始化:建立N个数据库连接缓存起来查询1:从缓存借到数据库连接发起查询收到响应归还数据库连接对象到缓存查询2:从缓存借到数据库连接发起查询收到响应归还数据库连接对象到缓存查询3:从缓存借到数据库连接发起查询收到响应归还数据库连接对象到缓存 使用池化思想后,数据库连接并不会频繁地创建关闭,而是启动后就初始化了N连接以供后续使用,使用完毕后归还对象,这样程序的总体性能得到提升。3。对象池的实现 通过上面的例子也可以发现池化思想的几个关键步骤:初始化、借出、归还。上面没有展示销毁步骤,某些场景下还需要对象地销毁这一过程,比如释放连接。 下面我们手动实现一个简陋的对象池,加深下对对象池的理解。主要是定一个对象池管理类,然后在里面实现对象的初始化、借出、归还、销毁等操作。packagecom。wdbyet。tool。objectpool。mypool;importjava。io。Closeable;importjava。io。IOException;importjava。util。HashSet;importjava。util。Stack;authorhttps:www。wdbyte。compublicclassMyObjectPoolTextendsCloseable{池子大小privateIntegersize5;对象池栈。后进先出privateStackTstackPoolnewStack();借出的对象的hashCode集合privateHashSetIntegerborrowHashCodeSetnewHashSet();增加一个对象paramtpublicsynchronizedvoidaddObj(Tt){if((stackPool。size()borrowHashCodeSet。size())size){thrownewRuntimeException(池中对象已经达到最大值);}stackPool。add(t);System。out。println(添加了对象:t。hashCode());}借出一个对象returnpublicsynchronizedTborrowObj(){if(stackPool。isEmpty()){System。out。println(没有可以被借出的对象);returnnull;}TpopstackPool。pop();borrowHashCodeSet。add(pop。hashCode());System。out。println(借出了对象:pop。hashCode());returnpop;}归还一个对象paramtpublicsynchronizedvoidreturnObj(Tt){if(borrowHashCodeSet。contains(t。hashCode())){stackPool。add(t);borrowHashCodeSet。remove(t。hashCode());System。out。println(归还了对象:t。hashCode());return;}thrownewRuntimeException(只能归还从池中借出的对象);}销毁池中对象publicsynchronizedvoiddestory(){if(!borrowHashCodeSet。isEmpty()){thrownewRuntimeException(尚有未归还的对象,不能关闭所有对象);}while(!stackPool。isEmpty()){TpopstackPool。pop();try{pop。close();}catch(IOExceptione){thrownewRuntimeException(e);}}System。out。println(已经销毁了所有对象);}}折叠 代码还是比较简单的,只是简单的示例,下面我们通过池化一个Redis连接对象Jedis来演示如何使用。 其实Jedis中已经有对应的Jedis池化管理对象了JedisPool了,不过我们这里为了演示对象池的实现,就不使用官方提供的JedisPool了。 启动一个Redis服务这里不做介绍,假设你已经有了一个Redis服务,下面引入Java中连接Redis需要用到的Maven依赖。dependencygroupIdredis。clientsgroupIdjedisartifactIdversion4。2。0versiondependency 正常情况下Jedis对象的使用方式:JedisjedisnewJedis(localhost,6379);Stringnamejedis。get(name);System。out。println(name);jedis。close(); 如果使用上面的对象池,就可以像下面这样使用。packagecom。wdbyet。tool。objectpool。mypool;importredis。clients。jedis。Jedis;authorniulangdate20220702publicclassMyObjectPoolTest{publicstaticvoidmain(String〔〕args){MyObjectPoolJedisobjectPoolnewMyObjectPool();增加一个jedis连接对象objectPool。addObj(newJedis(127。0。0。1,6379));objectPool。addObj(newJedis(127。0。0。1,6379));从对象池中借出一个jedis对象JedisjedisobjectPool。borrowObj();一次redis查询Stringnamejedis。get(name);System。out。println(String。format(redisget:name));归还redis连接对象objectPool。returnObj(jedis);销毁对象池中的所有对象objectPool。destory();再次借用对象objectPool。borrowObj();}} 输出日志:添加了对象:1556956098添加了对象:1252585652借出了对象:1252585652redisget:www。wdbyte。com归还了对象:1252585652已经销毁了所有对象没有可以被借出的对象 如果使用JMH对使用对象池化进行Redis查询,和正常创建Redis连接然后查询关闭连接的方式进行性能对比,会发现两者的性能差异很大。下面是测试结果,可以发现使用对象池化后的性能是非池化方式的5倍左右。BenchmarkModeCntScoreErrorUnitsMyObjectPoolTest。testthrpt152612。689358。767opssMyObjectPoolTest。testPoolthrpt912414。22811669。484opss4。开源的对象池工具 上面自己实现的对象池总归有些简陋了,其实开源工具中已经有了非常好用的对象池的实现,如Apache的commonspool2工具,很多开源工具中的对象池都是基于此工具实现,下面介绍这个工具的使用方式。 maven依赖:dependencygroupIdorg。apache。commonsgroupIdcommonspool2artifactIdversion2。11。1versiondependency 在commonspool2对象池工具中有几个关键的类。PooledObjectFactory类是一个工厂接口,用于实现想要池化对象的创建、验证、销毁等操作。GenericObjectPool类是一个通用的对象池管理类,可以进行对象的借出、归还等操作。GenericObjectPoolConfig类是对象池的配置类,可以进行对象的最大、最小等容量信息进行配置。 下面通过一个具体的示例演示commonspool2工具类的使用,这里依旧选择Redis连接对象Jedis作为演示。 实现PooledObjectFactory工厂类,实现其中的对象创建和销毁方法。publicclassMyPooledObjectFactoryimplementsPooledObjectFactoryJedis{OverridepublicvoidactivateObject(PooledObjectJedispooledObject)throwsException{}OverridepublicvoiddestroyObject(PooledObjectJedispooledObject)throwsException{JedisjedispooledObject。getObject();jedis。close();System。out。println(释放连接);}OverridepublicPooledObjectJedismakeObject()throwsException{returnnewDefaultPooledObject(newJedis(localhost,6379));}OverridepublicvoidpassivateObject(PooledObjectJedispooledObject)throwsException{}OverridepublicbooleanvalidateObject(PooledObjectJedispooledObject){returnfalse;}} 继承GenericObjectPool类,实现对对象的借出、归还等操作。publicclassMyGenericObjectPoolextendsGenericObjectPoolJedis{publicMyGenericObjectPool(PooledObjectFactoryfactory){super(factory);}publicMyGenericObjectPool(PooledObjectFactoryfactory,GenericObjectPoolConfigconfig){super(factory,config);}publicMyGenericObjectPool(PooledObjectFactoryfactory,GenericObjectPoolConfigconfig,AbandonedConfigabandonedConfig){super(factory,config,abandonedConfig);}} 可以看到MyGenericObjectPool类的构造函数中的入参有GenericObjectPoolConfig对象,这是个对象池的配置对象,可以配置对象池的容量大小等信息,这里就不配置了,使用默认配置。 通过GenericObjectPoolConfig的源码可以看到默认配置中,对象池的容量是8个。publicclassGenericObjectPoolConfigTextendsBaseObjectPoolConfigT{Thedefaultvalueforthe{codemaxTotal}configurationattribute。seeGenericObjectPoolgetMaxTotal()publicstaticfinalintDEFAULTMAXTOTAL8;Thedefaultvalueforthe{codemaxIdle}configurationattribute。seeGenericObjectPoolgetMaxIdle()publicstaticfinalintDEFAULTMAXIDLE8; 下面编写一个对象池使用测试类。publicclassApachePool{publicstaticvoidmain(String〔〕args)throwsException{MyGenericObjectPoolobjectMyObjectPoolnewMyGenericObjectPool(newMyPooledObjectFactory());JedisjedisobjectMyObjectPool。borrowObject();Stringnamejedis。get(name);System。out。println(name);objectMyObjectPool。returnObject(jedis);objectMyObjectPool。close();}} 输出日志:redisget:www。wdbyte。com释放连接 上面已经演示了commonspool2工具中的对象池的使用方式,从上面的例子中可以发现这种对象池中只能存放同一种初始化条件的对象,如果这里的Redis我们需要存储一个本地连接和一个远程连接的两种Jedis对象,就不能满足了。那么怎么办呢? 其实commonspool2工具已经考虑到了这种情况,通过增加一个key值可以在同一个对象池管理中进行区分,代码和上面类似,直接贴出完整的代码实现。packagecom。wdbyet。tool。objectpool。apachekeyedpool;importorg。apache。commons。pool2。BaseKeyedPooledObjectFactory;importorg。apache。commons。pool2。KeyedPooledObjectFactory;importorg。apache。commons。pool2。PooledObject;importorg。apache。commons。pool2。impl。AbandonedConfig;importorg。apache。commons。pool2。impl。DefaultPooledObject;importorg。apache。commons。pool2。impl。GenericKeyedObjectPool;importorg。apache。commons。pool2。impl。GenericKeyedObjectPoolConfig;importredis。clients。jedis。Jedis;authorhttps:www。wdbyte。comdate20220707publicclassApacheKeyedPool{publicstaticvoidmain(String〔〕args)throwsException{Stringkeylocal;MyGenericKeyedObjectPoolobjectMyObjectPoolnewMyGenericKeyedObjectPool(newMyKeyedPooledObjectFactory());JedisjedisobjectMyObjectPool。borrowObject(key);Stringnamejedis。get(name);System。out。println(redisget:name);objectMyObjectPool。returnObject(key,jedis);}}classMyKeyedPooledObjectFactoryextendsBaseKeyedPooledObjectFactoryString,Jedis{OverridepublicJediscreate(Stringkey)throwsException{if(local。equals(key)){returnnewJedis(localhost,6379);}if(remote。equals(key)){returnnewJedis(192。168。0。105,6379);}returnnull;}OverridepublicPooledObjectJediswrap(Jedisvalue){returnnewDefaultPooledObject(value);}}classMyGenericKeyedObjectPoolextendsGenericKeyedObjectPoolString,Jedis{publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedisfactory){super(factory);}publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedisfactory,GenericKeyedObjectPoolConfigJedisconfig){super(factory,config);}publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedisfactory,GenericKeyedObjectPoolConfigJedisconfig,AbandonedConfigabandonedConfig){super(factory,config,abandonedConfig);}}折叠 输出日志:redisget:www。wdbyte。com5。JedisPool对象池实现分析 这篇文章中的演示都使用了Jedis连接对象,其实在JedisSDK中已经实现了相应的对象池,也就是我们常用的JedisPool类。那么这里的JedisPool是怎么实现的呢?我们先看一下JedisPool的使用方式。packagecom。wdbyet。tool。objectpool;importredis。clients。jedis。Jedis;importredis。clients。jedis。JedisPool;authorhttps:www。wdbyte。compublicclassJedisPoolTest{publicstaticvoidmain(String〔〕args){JedisPooljedisPoolnewJedisPool(localhost,6379);从对象池中借一个对象JedisjedisjedisPool。getResource();Stringnamejedis。get(name);System。out。println(redisget:name);jedis。close();彻底退出前,关闭Redis连接池jedisPool。close();}} 代码中添加了注释,可以看到通过jedisPool。getResource()拿到了一个对象,这里和上面commonspool2工具中的borrowObject十分相似,继续追踪它的代码实现可以看到下面的代码。redis。clients。jedis。JedisPoolpublicclassJedisPoolextendsPoolJedis{publicJedisgetResource(){Jedisjedis(Jedis)super。getResource();jedis。setDataSource(this);returnjedis;}继续追踪super。getResource()redis。clients。jedis。util。PoolpublicTgetResource(){try{returnsuper。borrowObject();}catch(JedisExceptionvar2){throwvar2;}catch(Exceptionvar3){thrownewJedisException(Couldnotgetaresourcefromthepool,var3);}} 竟然看到了super。borrowObject(),多么熟悉的方法,继续分析代码可以发现Jedis对象池也是适用了commonspool2工具作为实现。既然如此,那么jedis。close()方法的逻辑我们应该也可以猜到了,应该有一个归还的操作,查看代码发现果然如此。redis。clients。jedis。JedisPoolpublicclassJedisPoolextendsPoolJedis{publicvoidclose(){if(this。dataSource!null){PoolJedispoolthis。dataSource;this。dataSourcenull;if(this。isBroken()){pool。returnBrokenResource(this);}else{pool。returnResource(this);}}else{this。connection。close();}}继续追踪super。getResource()redis。clients。jedis。util。PoolpublicvoidreturnResource(Tresource){if(resource!null){try{super。returnObject(resource);}catch(RuntimeExceptionvar3){thrownewJedisException(Couldnotreturntheresourcetothepool,var3);}}} 通过上面的分析,可见Jedis确实使用了commonspool2工具进行对象池的管理,通过分析JedisPool类的继承关系图也可以发现。 6。对象池总结 通过这篇文章的介绍,可以发现池化思想有几个明显的优势。可以显著的提高应用程序的性能。如果一个对象创建成本过高,那么使用池化非常有效。池化提供了一种对象的管理以及重复使用的方式,减少内存碎片。可以为对象的创建数量提供限制,对某些对象不能创建过多的场景提供保护。 但是使用对象池化也有一些需要注意的地方,比如归还对象时应确保对象已经被重置为可以重复使用的状态。同时也要注意,使用池化时要根据具体的场景合理的设置池子的大小,过小达不到想要的效果,过大会造成内存浪费。 一如既往,文章中代码存放在Github。comniumoojavaNotes。 原文链接:https:www。cnblogs。comniumoop16472756。html