Java象池如何实现,为何需要对象池?
最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识。
1. 什么是对象池
池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以使用。对象池大多数场景下都是缓存着创建成本过高或者需要重复创建使用的对象,从池子中取对象的时间是可以预测的,但是新建一个对象的时间是不确定的。
当需要一个新对象时,就向池中借出一个,然后对象池标记当前对象正在使用,使用完毕后归还到对象池,以便再次借出。
常见的使用对象池化场景:
- 1. 对象创建成本过高。
- 2. 需要频繁的创建大量重复对象,会产生很多内存碎片。
- 3. 同时使用的对象不会太多。
- 4. 常见的具体场景如数据库连接池、线程池等。
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.com */ publicclassMyObjectPoolTextendsCloseable> { //池子大小 privateIntegersize=5; //对象池栈。后进先出 privateStackT> stackPool=newStack> (); //借出的对象的hashCode集合 privateHashSetInteger> borrowHashCodeSet=newHashSet> (); /** *增加一个对象 * *@paramt */ publicsynchronizedvoidaddObj(Tt){ if((stackPool.size()+borrowHashCodeSet.size())==size){ thrownewRuntimeException("池中对象已经达到最大值"); } stackPool.add(t); System.out.println("添加了对象:"+t.hashCode()); } /** *借出一个对象 * *@return */ publicsynchronizedTborrowObj(){ if(stackPool.isEmpty()){ System.out.println("没有可以被借出的对象"); returnnull; } Tpop=stackPool.pop(); borrowHashCodeSet.add(pop.hashCode()); System.out.println("借出了对象:"+pop.hashCode()); returnpop; } /** *归还一个对象 * *@paramt */ publicsynchronizedvoidreturnObj(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()){ Tpop=stackPool.pop(); try{ pop.close(); } catch(IOExceptione){ thrownewRuntimeException(e); } } System.out.println("已经销毁了所有对象"); } }
代码还是比较简单的,只是简单的示例,下面我们通过池化一个 Redis 连接对象 Jedis 来演示如何使用。
其实 Jedis 中已经有对应的 Jedis 池化管理对象了 JedisPool 了,不过我们这里为了演示对象池的实现,就不使用官方提供的 JedisPool 了。
启动一个 Redis 服务这里不做介绍,假设你已经有了一个 Redis 服务,下面引入 Java 中连接 Redis 需要用到的 Maven 依赖。
dependency> groupId> redis.clients/groupId> artifactId> jedis/artifactId> version> 4.2.0/version> /dependency>
正常情况下 Jedis 对象的使用方式:
Jedisjedis=newJedis("localhost",6379); Stringname=jedis.get("name"); System.out.println(name); jedis.close();
如果使用上面的对象池,就可以像下面这样使用。
packagecom.wdbyet.tool.objectpool.mypool; importredis.clients.jedis.Jedis; /** *@authorniulang *@date2022/07/02 */ publicclassMyObjectPoolTest{ publicstaticvoidmain(String[]args){ MyObjectPoolJedis> objectPool=newMyObjectPool> (); //增加一个jedis连接对象 objectPool.addObj(newJedis("127.0.0.1",6379)); objectPool.addObj(newJedis("127.0.0.1",6379)); //从对象池中借出一个jedis对象 Jedisjedis=objectPool.borrowObj(); //一次redis查询 Stringname=jedis.get("name"); System.out.println(String.format("redisget:"+name)); //归还redis连接对象 objectPool.returnObj(jedis); //销毁对象池中的所有对象 objectPool.destory(); //再次借用对象 objectPool.borrowObj(); } }
输出日志:
添加了对象:1556956098
添加了对象:1252585652
借出了对象:1252585652
redisget:www.wdbyte.com
归还了对象:1252585652
已经销毁了所有对象
没有可以被借出的对象
如果使用 JMH 对使用对象池化进行 Redis 查询,和正常创建 Redis 连接然后查询关闭连接的方式进行性能对比,会发现两者的性能差异很大。下面是测试结果,可以发现使用对象池化后的性能是非池化方式的 5 倍左右。
BenchmarkModeCntScoreErrorUnits
MyObjectPoolTest.testthrpt152612.689±358.767ops/s
MyObjectPoolTest.testPoolthrpt912414.228±11669.484ops/s
4. 开源的对象池工具
上面自己实现的对象池总归有些简陋了,其实开源工具中已经有了非常好用的对象池的实现,如 Apache 的commons-pool2
工具,很多开源工具中的对象池都是基于此工具实现,下面介绍这个工具的使用方式。
maven 依赖:
dependency> groupId> org.apache.commons/groupId> artifactId> commons-pool2/artifactId> version> 2.11.1/version> /dependency>
在commons-pool2
对象池工具中有几个关键的类。
- •
PooledObjectFactory
类是一个工厂接口,用于实现想要池化对象的创建、验证、销毁等操作。 - •
GenericObjectPool
类是一个通用的对象池管理类,可以进行对象的借出、归还等操作。 - •
GenericObjectPoolConfig
类是对象池的配置类,可以进行对象的最大、最小等容量信息进行配置。
下面通过一个具体的示例演示commons-pool2
工具类的使用,这里依旧选择 Redis 连接对象 Jedis 作为演示。
实现PooledObjectFactory
工厂类,实现其中的对象创建和销毁方法。
publicclassMyPooledObjectFactoryimplementsPooledObjectFactoryJedis> { @Override publicvoidactivateObject(PooledObjectJedis> pooledObject)throwsException{ } @Override publicvoiddestroyObject(PooledObjectJedis> pooledObject)throwsException{ Jedisjedis=pooledObject.getObject(); jedis.close(); System.out.println("释放连接"); } @Override publicPooledObjectJedis> makeObject()throwsException{ returnnewDefaultPooledObject(newJedis("localhost",6379)); } @Override publicvoidpassivateObject(PooledObjectJedis> pooledObject)throwsException{ } @Override publicbooleanvalidateObject(PooledObjectJedis> pooledObject){ 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 个。
publicclassGenericObjectPoolConfigT> extendsBaseObjectPoolConfigT> { /** *Thedefaultvalueforthe{ @codemaxTotal} configurationattribute. *@seeGenericObjectPool#getMaxTotal() */ publicstaticfinalintDEFAULT_MAX_TOTAL=8; /** *Thedefaultvalueforthe{ @codemaxIdle} configurationattribute. *@seeGenericObjectPool#getMaxIdle() */ publicstaticfinalintDEFAULT_MAX_IDLE=8;
下面编写一个对象池使用测试类。
publicclassApachePool{ publicstaticvoidmain(String[]args)throwsException{ MyGenericObjectPoolobjectMyObjectPool=newMyGenericObjectPool(newMyPooledObjectFactory()); Jedisjedis=objectMyObjectPool.borrowObject(); Stringname=jedis.get("name"); System.out.println(name); objectMyObjectPool.returnObject(jedis); objectMyObjectPool.close(); } }
输出日志:
redisget:www.wdbyte.com
释放连接
上面已经演示了commons-pool2
工具中的对象池的使用方式,从上面的例子中可以发现这种对象池中只能存放同一种初始化条件的对象,如果这里的 Redis 我们需要存储一个本地连接和一个远程连接的两种 Jedis 对象,就不能满足了。那么怎么办呢?
其实commons-pool2
工具已经考虑到了这种情况,通过增加一个 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.com *@date2022/07/07 */ publicclassApacheKeyedPool{ publicstaticvoidmain(String[]args)throwsException{ Stringkey="local"; MyGenericKeyedObjectPoolobjectMyObjectPool=newMyGenericKeyedObjectPool(newMyKeyedPooledObjectFactory()); Jedisjedis=objectMyObjectPool.borrowObject(key); Stringname=jedis.get("name"); System.out.println("redisget:"+name); objectMyObjectPool.returnObject(key,jedis); } } classMyKeyedPooledObjectFactoryextendsBaseKeyedPooledObjectFactoryString,Jedis> { @Override publicJediscreate(Stringkey)throwsException{ if("local".equals(key)){ returnnewJedis("localhost",6379); } if("remote".equals(key)){ returnnewJedis("192.168.0.105",6379); } returnnull; } @Override publicPooledObjectJedis> wrap(Jedisvalue){ returnnewDefaultPooledObject> (value); } } classMyGenericKeyedObjectPoolextendsGenericKeyedObjectPoolString,Jedis> { publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedis> factory){ super(factory); } publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedis> factory, GenericKeyedObjectPoolConfigJedis> config){ super(factory,config); } publicMyGenericKeyedObjectPool(KeyedPooledObjectFactoryString,Jedis> factory, GenericKeyedObjectPoolConfigJedis> config,AbandonedConfigabandonedConfig){ super(factory,config,abandonedConfig); } }
输出日志:
redis get :www.wdbyte.com
5. JedisPool 对象池实现分析
这篇文章中的演示都使用了 Jedis 连接对象,其实在 Jedis SDK 中已经实现了相应的对象池,也就是我们常用的 JedisPool 类。那么这里的 JedisPool 是怎么实现的呢?我们先看一下 JedisPool 的使用方式。
packagecom.wdbyet.tool.objectpool; importredis.clients.jedis.Jedis; importredis.clients.jedis.JedisPool; /** *@authorhttps://www.wdbyte.com */ publicclassJedisPoolTest{ publicstaticvoidmain(String[]args){ JedisPooljedisPool=newJedisPool("localhost",6379); //从对象池中借一个对象 Jedisjedis=jedisPool.getResource(); Stringname=jedis.get("name"); System.out.println("redisget:"+name); jedis.close(); //彻底退出前,关闭Redis连接池 jedisPool.close(); } }
代码中添加了注释,可以看到通过jedisPool.getResource()
拿到了一个对象,这里和上面commons-pool2
工具中的borrowObject
十分相似,继续追踪它的代码实现可以看到下面的代码。
//redis.clients.jedis.JedisPool //publicclassJedisPoolextendsPoolJedis> { publicJedisgetResource(){ Jedisjedis=(Jedis)super.getResource(); jedis.setDataSource(this); returnjedis; } //继续追踪super.getResource() //redis.clients.jedis.util.Pool publicTgetResource(){ try{ returnsuper.borrowObject(); } catch(JedisExceptionvar2){ throwvar2; } catch(Exceptionvar3){ thrownewJedisException("Couldnotgetaresourcefromthepool",var3); } }
竟然看到了super.borrowObject()
,多么熟悉的方法,继续分析代码可以发现 Jedis 对象池也是使用了commons-pool2
工具作为实现。既然如此,那么jedis.close()
方法的逻辑我们应该也可以猜到了,应该有一个归还的操作,查看代码发现果然如此。
//redis.clients.jedis.JedisPool //publicclassJedisPoolextendsPoolJedis> { publicvoidclose(){ if(this.dataSource!=null){ PoolJedis> pool=this.dataSource; this.dataSource=null; if(this.isBroken()){ pool.returnBrokenResource(this); } else{ pool.returnResource(this); } } else{ this.connection.close(); } } //继续追踪super.getResource() //redis.clients.jedis.util.Pool publicvoidreturnResource(Tresource){ if(resource!=null){ try{ super.returnObject(resource); } catch(RuntimeExceptionvar3){ thrownewJedisException("Couldnotreturntheresourcetothepool",var3); } } }
通过上面的分析,可见 Jedis 确实使用了commons-pool2
工具进行对象池的管理,通过分析 JedisPool 类的继承关系图也可以发现。
JedisPool 继承关系
6. 对象池总结
通过这篇文章的介绍,可以发现池化思想有几个明显的优势。
- 1. 可以显著的提高应用程序的性能。
- 2. 如果一个对象创建成本过高,那么使用池化非常有效。
- 3. 池化提供了一种对象的管理以及重复使用的方式,减少内存碎片。
- 4. 可以为对象的创建数量提供限制,对某些对象不能创建过多的场景提供保护。
但是使用对象池化也有一些需要注意的地方,比如归还对象时应确保对象已经被重置为可以重复使用的状态。同时也要注意,使用池化时要根据具体的场景合理的设置池子的大小,过小达不到想要的效果,过大会造成内存浪费。
以上就是关于Java象池如何实现,为何需要对象池?的介绍,本文内容仅供参考,有需要的朋友可以借鉴了解看看,希望对大家学习或工作,想要了解更多欢迎关注网络,小编每天都会为大家更新不同的知识。
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: Java象池如何实现,为何需要对象池?
本文地址: https://pptw.com/jishu/652341.html