最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识。
1. 什么是对象池池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以使用。对象池大多数场景下都是缓存着创建成本过高或者需要重复创建使用的对象,从池子中取对象的时间是可以预测的,但是新建一个对象的时间是不确定的。
当需要一个新对象时,就向池中借出一个,然后对象池标记当前对象正在使用,使用完毕后归还到对象池,以便再次借出。
常见的使用对象池化场景:
1. 对象创建成本过高。2. 需要频繁的创建大量重复对象,会产生很多内存碎片。3. 同时使用的对象不会太多。4. 常见的具体场景如数据库连接池、线程池等。 2. 为什么需要对象池如果一个对象的创建成本很高,比如建立数据库的连接时耗时过长,在不使用池化技术的情况下,我们的查询过程可能是这样的。
查询 1:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接查询 2:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接查询 3:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接在这种模式下,每次查询都要重新建立关闭连接,因为建立连接是一个耗时的操作,所以这种模式会影响程序的总体性能。
那么使用池化思想是怎么样的呢?同样的过程会转变成下面的步骤。
初始化:建立 N 个数据库连接 -> 缓存起来查询 1:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存查询 2:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存查询 3:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存使用池化思想后,数据库连接并不会频繁的创建关闭,而是启动后就初始化了 N 个连接以供后续使用,使用完毕后归还对象,这样程序的总体性能得到提升。
3. 对象池的实现通过上面的例子也可以发现池化思想的几个关键步骤:初始化、借出、归还。上面没有展示销毁步骤, 某些场景下还需要对象的销毁这一过程,比如释放连接。
下面我们手动实现一个简陋的对象池,加深下对对象池的理解。主要是定一个对象池管理类,然后在里面实现对象的初始化、借出、归还、销毁等操作。
代码还是比较简单的,只是简单的示例,下面我们通过池化一个 Redis 连接对象 Jedis 来演示如何使用。
其实 Jedis 中已经有对应的 Jedis 池化管理对象了 JedisPool 了,不过我们这里为了演示对象池的实现,就不使用官方提供的 JedisPool 了。
启动一个 Redis 服务这里不做介绍,假设你已经有了一个 Redis 服务,下面引入 Java 中连接 Redis 需要用到的 Maven 依赖。
正常情况下 Jedis 对象的使用方式:
如果使用上面的对象池,就可以像下面这样使用。
输出日志:
添加了对象:1556956098添加了对象:1252585652借出了对象:1252585652redis get:www.wdbyte.com归还了对象:1252585652已经销毁了所有对象没有可以被借出的对象
如果使用 JMH 对使用对象池化进行 Redis 查询,和正常创建 Redis 连接然后查询关闭连接的方式进行性能对比,会发现两者的性能差异很大。下面是测试结果,可以发现使用对象池化后的性能是非池化方式的 5 倍左右。
Benchmark Mode Cnt Score Error UnitsMyObjectPoolTest.test thrpt 15 2612.689 ± 358.767 ops/sMyObjectPoolTest.testPool thrpt 9 12414.228 ± 11669.484 ops/s
4. 开源的对象池工具上面自己实现的对象池总归有些简陋了,其实开源工具中已经有了非常好用的对象池的实现,如 Apache 的 commons-pool2 工具,很多开源工具中的对象池都是基于此工具实现,下面介绍这个工具的使用方式。
maven 依赖:
在 commons-pool2 对象池工具中有几个关键的类。
• PooledObjectFactory 类是一个工厂接口,用于实现想要池化对象的创建、验证、销毁等操作。• GenericObjectPool 类是一个通用的对象池管理类,可以进行对象的借出、归还等操作。• GenericObjectPoolConfig 类是对象池的配置类,可以进行对象的最大、最小等容量信息进行配置。下面通过一个具体的示例演示 commons-pool2 工具类的使用,这里依旧选择 Redis 连接对象 Jedis 作为演示。
实现 PooledObjectFactory 工厂类,实现其中的对象创建和销毁方法。
继承 GenericObjectPool 类,实现对对象的借出、归还等操作。
可以看到 MyGenericObjectPool 类的构造函数中的入参有 GenericObjectPoolConfig 对象,这是个对象池的配置对象,可以配置对象池的容量大小等信息,这里就不配置了,使用默认配置。
通过 GenericObjectPoolConfig 的源码可以看到默认配置中,对象池的容量是 8 个。
下面编写一个对象池使用测试类。
输出日志:
redis get:www.wdbyte.com释放连接
上面已经演示了 commons-pool2 工具中的对象池的使用方式,从上面的例子中可以发现这种对象池中只能存放同一种初始化条件的对象,如果这里的 Redis 我们需要存储一个本地连接和一个远程连接的两种 Jedis 对象,就不能满足了。那么怎么办呢?
其实 commons-pool2 工具已经考虑到了这种情况,通过增加一个 key 值可以在同一个对象池管理中进行区分,代码和上面类似,直接贴出完整的代码实现。
输出日志:
redis get :www.wdbyte.com
5. JedisPool 对象池实现分析这篇文章中的演示都使用了 Jedis 连接对象,其实在 Jedis SDK 中已经实现了相应的对象池,也就是我们常用的 JedisPool 类。那么这里的 JedisPool 是怎么实现的呢?我们先看一下 JedisPool 的使用方式。
代码中添加了注释,可以看到通过 jedisPool.getResource() 拿到了一个对象,这里和上面 commons-pool2 工具中的 borrowObject 十分相似,继续追踪它的代码实现可以看到下面的代码。
竟然看到了 super.borrowObject() ,多么熟悉的方法,继续分析代码可以发现 Jedis 对象池也是使用了 commons-pool2 工具作为实现。既然如此,那么 jedis.close() 方法的逻辑我们应该也可以猜到了,应该有一个归还的操作,查看代码发现果然如此。
通过上面的分析,可见 Jedis 确实使用了 commons-pool2 工具进行对象池的管理,通过分析 JedisPool 类的继承关系图也可以发现。
JedisPool 继承关系
6. 对象池总结通过这篇文章的介绍,可以发现池化思想有几个明显的优势。
1. 可以显著的提高应用程序的性能。2. 如果一个对象创建成本过高,那么使用池化非常有效。3. 池化提供了一种对象的管理以及重复使用的方式,减少内存碎片。4. 可以为对象的创建数量提供限制,对某些对象不能创建过多的场景提供保护。但是使用对象池化也有一些需要注意的地方,比如归还对象时应确保对象已经被重置为可以重复使用的状态。同时也要注意,使用池化时要根据具体的场景合理的设置池子的大小,过小达不到想要的效果,过大会造成内存浪费。
以上就是一文搞懂Java中对象池的实现的详细内容,更多关于Java对象池的资料请关注七叶笔记其它相关文章!