点赞再看,动力无限。Hello world : ) 微信搜「 程序猿阿朗 」。

本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章。

最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 8ms 左右时间,分析后发现这个对象可以通过对象池模式进行优化,优化后此步耗时仅有 0.01ms,这篇文章介绍对象池相关知识。

1. 什么是对象池

池化并不是什么新鲜的技术,它更像一种软件设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以使用。对象池大多数场景下都是缓存着创建成本过高或者需要重复创建使用的对象,从池子中取对象的时间是可以预测的,但是新建一个对象的时间是不确定的。

当需要一个新对象时,就向池中借出一个,然后对象池标记当前对象正在使用,使用完毕后归还到对象池,以便再次借出。

常见的使用对象池化场景:

  1. 对象创建成本过高。
  2. 需要频繁的创建大量重复对象,会产生很多内存碎片。
  3. 同时使用的对象不会太多。
  4. 常见的具体场景如数据库连接池、线程池等。

2. 为什么需要对象池

如果一个对象的创建成本很高,比如建立数据库的连接时耗时过长,在不使用池化技术的情况下,我们的查询过程可能是这样的。

查询 1:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接
查询 2:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接
查询 3:建立数据库连接 -> 发起查询 -> 收到响应 -> 关闭连接

在这种模式下,每次查询都要重新建立关闭连接,因为建立连接是一个耗时的操作,所以这种模式会影响程序的总体性能。

那么使用池化思想是怎么样的呢?同样的过程会转变成下面的步骤。

初始化:建立 N 个数据库连接 -> 缓存起来
查询 1:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存
查询 2:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存
查询 3:从缓存借到数据库连接 -> 发起查询 -> 收到响应 -> 归还数据库连接对象到缓存

使用池化思想后,数据库连接并不会频繁的创建关闭,而是启动后就初始化了 N 个连接以供后续使用,使用完毕后归还对象,这样程序的总体性能得到提升。

3. 对象池的实现

通过上面的例子也可以发现池化思想的几个关键步骤:初始化、借出、归还。上面没有展示销毁步骤, 某些场景下还需要对象的销毁这一过程,比如释放连接。

下面我们手动实现一个简陋的对象池,加深下对对象池的理解。主要是定一个对象池管理类,然后在里面实现对象的初始化、借出、归还、销毁等操作。

package com.wdbyet.tool.objectpool.mypool;import java.io.Closeable;
import java.io.IOException;
import java.util.HashSet;
import java.util.Stack;/*** @author https://www.wdbyte.com*/
public class MyObjectPool<T extends Closeable> {// 池子大小private Integer size = 5;// 对象池栈。后进先出private Stack<T> stackPool = new Stack<>();// 借出的对象的 hashCode 集合private HashSet<Integer> borrowHashCodeSet = new HashSet<>();/*** 增加一个对象** @param t*/public synchronized void addObj(T t) {if ((stackPool.size() + borrowHashCodeSet.size()) == size) {throw new RuntimeException("池中对象已经达到最大值");}stackPool.add(t);System.out.println("添加了对象:" + t.hashCode());}/*** 借出一个对象** @return*/public synchronized T borrowObj() {if (stackPool.isEmpty()) {System.out.println("没有可以被借出的对象");return null;}T pop = stackPool.pop();borrowHashCodeSet.add(pop.hashCode());System.out.println("借出了对象:" + pop.hashCode());return pop;}/*** 归还一个对象** @param t*/public synchronized void returnObj(T t) {if (borrowHashCodeSet.contains(t.hashCode())) {stackPool.add(t);borrowHashCodeSet.remove(t.hashCode());System.out.println("归还了对象:" + t.hashCode());return;}throw new RuntimeException("只能归还从池中借出的对象");}/*** 销毁池中对象*/public synchronized void destory() {if (!borrowHashCodeSet.isEmpty()) {throw new RuntimeException("尚有未归还的对象,不能关闭所有对象");}while (!stackPool.isEmpty()) {T pop = stackPool.pop();try {pop.close();} catch (IOException e) {throw new RuntimeException(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 对象的使用方式:

Jedis jedis = new Jedis("localhost", 6379);
String name = jedis.get("name");
System.out.println(name);
jedis.close();

如果使用上面的对象池,就可以像下面这样使用。

package com.wdbyet.tool.objectpool.mypool;import redis.clients.jedis.Jedis;/*** @author niulang* @date 2022/07/02*/
public class MyObjectPoolTest {public static void main(String[] args) {MyObjectPool<Jedis> objectPool = new MyObjectPool<>();// 增加一个 jedis 连接对象objectPool.addObj(new Jedis("127.0.0.1", 6379));objectPool.addObj(new Jedis("127.0.0.1", 6379));// 从对象池中借出一个 jedis 对象Jedis jedis = objectPool.borrowObj();// 一次 redis 查询String name = jedis.get("name");System.out.println(String.format("redis get:" + name));// 归还 redis 连接对象objectPool.returnObj(jedis);// 销毁对象池中的所有对象objectPool.destory();// 再次借用对象objectPool.borrowObj();}
}

输出日志:

添加了对象:1556956098
添加了对象:1252585652
借出了对象:1252585652
redis get:www.wdbyte.com
归还了对象:1252585652
已经销毁了所有对象
没有可以被借出的对象

如果使用 JMH 对使用对象池化进行 Redis 查询,和正常创建 Redis 连接然后查询关闭连接的方式进行性能对比,会发现两者的性能差异很大。下面是测试结果,可以发现使用对象池化后的性能是非池化方式的 5 倍左右。

Benchmark                   Mode  Cnt      Score       Error  Units
MyObjectPoolTest.test      thrpt   15   2612.689 ±   358.767  ops/s
MyObjectPoolTest.testPool  thrpt    9  12414.228 ± 11669.484  ops/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 工厂类,实现其中的对象创建和销毁方法。

public class MyPooledObjectFactory implements PooledObjectFactory<Jedis> {@Overridepublic void activateObject(PooledObject<Jedis> pooledObject) throws Exception {}@Overridepublic void destroyObject(PooledObject<Jedis> pooledObject) throws Exception {Jedis jedis = pooledObject.getObject();jedis.close();System.out.println("释放连接");}@Overridepublic PooledObject<Jedis> makeObject() throws Exception {return new DefaultPooledObject(new Jedis("localhost", 6379));}@Overridepublic void passivateObject(PooledObject<Jedis> pooledObject) throws Exception {}@Overridepublic boolean validateObject(PooledObject<Jedis> pooledObject) {return false;}
}

继承 GenericObjectPool 类,实现对对象的借出、归还等操作。

public class MyGenericObjectPool extends GenericObjectPool<Jedis> {public MyGenericObjectPool(PooledObjectFactory factory) {super(factory);}public MyGenericObjectPool(PooledObjectFactory factory, GenericObjectPoolConfig config) {super(factory, config);}public MyGenericObjectPool(PooledObjectFactory factory, GenericObjectPoolConfig config,AbandonedConfig abandonedConfig) {super(factory, config, abandonedConfig);}
}

可以看到 MyGenericObjectPool 类的构造函数中的入参有 GenericObjectPoolConfig 对象,这是个对象池的配置对象,可以配置对象池的容量大小等信息,这里就不配置了,使用默认配置。

通过 GenericObjectPoolConfig 的源码可以看到默认配置中,对象池的容量是 8 个

public class GenericObjectPoolConfig<T> extends BaseObjectPoolConfig<T> {/*** The default value for the {@code maxTotal} configuration attribute.* @see GenericObjectPool#getMaxTotal()*/public static final int DEFAULT_MAX_TOTAL = 8;/*** The default value for the {@code maxIdle} configuration attribute.* @see GenericObjectPool#getMaxIdle()*/public static final int DEFAULT_MAX_IDLE = 8;

下面编写一个对象池使用测试类。

public class ApachePool {public static void main(String[] args) throws Exception {MyGenericObjectPool objectMyObjectPool = new MyGenericObjectPool(new MyPooledObjectFactory());Jedis jedis = objectMyObjectPool.borrowObject();String name = jedis.get("name");System.out.println(name);objectMyObjectPool.returnObject(jedis);objectMyObjectPool.close();}}

输出日志:

redis get:www.wdbyte.com
释放连接

上面已经演示了 commons-pool2 工具中的对象池的使用方式,从上面的例子中可以发现这种对象池中只能存放同一种初始化条件的对象,如果这里的 Redis 我们需要存储一个本地连接和一个远程连接的两种 Jedis 对象,就不能满足了。那么怎么办呢?

其实 commons-pool2 工具已经考虑到了这种情况,通过增加一个 key 值可以在同一个对象池管理中进行区分,代码和上面类似,直接贴出完整的代码实现。

package com.wdbyet.tool.objectpool.apachekeyedpool;import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.KeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.AbandonedConfig;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import redis.clients.jedis.Jedis;/*** @author https://www.wdbyte.com* @date 2022/07/07*/
public class ApacheKeyedPool {public static void main(String[] args) throws Exception {String key = "local";MyGenericKeyedObjectPool objectMyObjectPool = new MyGenericKeyedObjectPool(new MyKeyedPooledObjectFactory());Jedis jedis = objectMyObjectPool.borrowObject(key);String name = jedis.get("name");System.out.println("redis get :" + name);objectMyObjectPool.returnObject(key, jedis);}
}class MyKeyedPooledObjectFactory extends BaseKeyedPooledObjectFactory<String, Jedis> {@Overridepublic Jedis create(String key) throws Exception {if ("local".equals(key)) {return new Jedis("localhost", 6379);}if ("remote".equals(key)) {return new Jedis("192.168.0.105", 6379);}return null;}@Overridepublic PooledObject<Jedis> wrap(Jedis value) {return new DefaultPooledObject<>(value);}
}class MyGenericKeyedObjectPool extends GenericKeyedObjectPool<String, Jedis> {public MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String, Jedis> factory) {super(factory);}public MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String, Jedis> factory,GenericKeyedObjectPoolConfig<Jedis> config) {super(factory, config);}public MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String, Jedis> factory,GenericKeyedObjectPoolConfig<Jedis> config, AbandonedConfig abandonedConfig) {super(factory, config, abandonedConfig);}
}

输出日志:

redis get :www.wdbyte.com

5. JedisPool 对象池实现分析

这篇文章中的演示都使用了 Jedis 连接对象,其实在 Jedis SDK 中已经实现了相应的对象池,也就是我们常用的 JedisPool 类。那么这里的 JedisPool 是怎么实现的呢?我们先看一下 JedisPool 的使用方式。

package com.wdbyet.tool.objectpool;import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;/*** @author https://www.wdbyte.com*/
public class JedisPoolTest {public static void main(String[] args) {JedisPool jedisPool = new JedisPool("localhost", 6379);// 从对象池中借一个对象Jedis jedis = jedisPool.getResource();String name = jedis.get("name");System.out.println("redis get :" + name);jedis.close();// 彻底退出前,关闭 Redis 连接池jedisPool.close();}
}

代码中添加了注释,可以看到通过 jedisPool.getResource() 拿到了一个对象,这里和上面 commons-pool2 工具中的 borrowObject 十分相似,继续追踪它的代码实现可以看到下面的代码。

// redis.clients.jedis.JedisPool
// public class JedisPool extends Pool<Jedis> {public Jedis getResource() {Jedis jedis = (Jedis)super.getResource();jedis.setDataSource(this);return jedis;
}
// 继续追踪 super.getResource()
// redis.clients.jedis.util.Pool
public T getResource() {try {return super.borrowObject();} catch (JedisException var2) {throw var2;} catch (Exception var3) {throw new JedisException("Could not get a resource from the pool", var3);}
}

竟然看到了 super.borrowObject() ,多么熟悉的方法,继续分析代码可以发现 Jedis 对象池也是适用了 commons-pool2 工具作为实现。既然如此,那么 jedis.close() 方法的逻辑我们应该也可以猜到了,应该有一个归还的操作,查看代码发现果然如此。

// redis.clients.jedis.JedisPool
// public class JedisPool extends Pool<Jedis> {public void close() {if (this.dataSource != null) {Pool<Jedis> 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
public void returnResource(T resource) {if (resource != null) {try {super.returnObject(resource);} catch (RuntimeException var3) {throw new JedisException("Could not return the resource to the pool", var3);}}
}

通过上面的分析,可见 Jedis 确实使用了 commons-pool2 工具进行对象池的管理,通过分析 JedisPool 类的继承关系图也可以发现。

6. 对象池总结

通过这篇文章的介绍,可以发现池化思想有几个明显的优势。

  1. 可以显著的提高应用程序的性能。
  2. 如果一个对象创建成本过高,那么使用池化非常有效。
  3. 池化提供了一种对象的管理以及重复使用的方式,减少内存碎片。
  4. 可以为对象的创建数量提供限制,对某些对象不能创建过多的场景提供保护。

但是使用对象池化也有一些需要注意的地方,比如归还对象时应确保对象已经被重置为可以重复使用的状态。同时也要注意,使用池化时要根据具体的场景合理的设置池子的大小,过小达不到想要的效果,过大会造成内存浪费。

一如既往,文章中代码存放在 Github.com/niumoo/javaNotes.

<完>

文章持续更新,可以微信搜一搜「 程序猿阿朗 」或访问「程序猿阿朗博客 」第一时间阅读。本文 Github.com/niumoo/JavaNotes 已经收录,有很多知识点和系列文章,欢迎Star。

Java 中的对象池实现相关推荐

  1. java中String对象和String变量

    2019独角兽企业重金招聘Python工程师标准>>> java中String对象和String变量 (2011-12-27 20:40:27) 转载▼ 标签: it 最近在论坛上看 ...

  2. 好好说说Java中的常量池之Class常量池

    前言 在Java中,常量池的概念想必很多人都听说过.这也是面试中比较常考的题目之一.在Java有关的面试题中,一般习惯通过String的有关问题来考察面试者对于常量池的知识的理解,几道简单的Strin ...

  3. 好好说说Java中的常量池之Class常量池 1

    转载自   好好说说Java中的常量池之Class常量池 在Java中,常量池的概念想必很多人都听说过.这也是面试中比较常考的题目之一.在Java有关的面试题中,一般习惯通过String的有关问题来考 ...

  4. java中String对象作为参数传递问题

    问题 java中将对象作为参数传递究竟是值传递还是引用传递? 1.基本类型作为参数传递时,是传递值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的. 2.对象作为参数传递时,是把对象在内存中的地址拷贝 ...

  5. 万字图文 | 学会Java中的线程池,这一篇也许就够了!

    来源:一枝花算不算浪漫 线程池原理思维导图.png 前言 Java中的线程池已经不是什么神秘的技术了,相信在看的读者在项目中也都有使用过.关于线程池的文章也是数不胜数,我们站在巨人的肩膀上来再次梳理一 ...

  6. Java中整数常量池的概念

    Java中整数常量池的概念: java中为了提高程序的执行效率,将[-128, 127]之间256个整数所有的包装对象提前创建好了,类加载时就已经创好了,放在了一个方法区的"整数常量池&qu ...

  7. JAVA中创建线程池的五种方法及比较

    之前写过JAVA中创建线程的三种方法及比较.这次来说说线程池. JAVA中创建线程池主要有两类方法,一类是通过Executors工厂类提供的方法,该类提供了4种不同的线程池可供使用.另一类是通过Thr ...

  8. 【性能优化】面试官:Java中的对象和数组都是在堆上分配的吗?

    写在前面 从开始学习Java的时候,我们就接触了这样一种观点:Java中的对象是在堆上创建的,对象的引用是放在栈里的,那这个观点就真的是正确的吗?如果是正确的,那么,面试官为啥会问:"Jav ...

  9. Java中的对象序列化操作

    文章目录 1 Java中的对象序列化操作 1 Java中的对象序列化操作 首先看一下对象序列化的操作步骤: 创建一个类,继承Serializable接口 创建对象 将对象写入文件 从文件读取对象信息 ...

最新文章

  1. 百度云使用第三方工具下载文件
  2. Python通过amqp消息队列协议中的Qpid实现数据通信
  3. uva 1623——Enter The Dragon
  4. 拿下 Gartner 容器产品第一,阿里云打赢云原生关键一战
  5. python 发邮件_Python发邮件告别smtplib,迎接zmail
  6. STP端口状态特点、STP端口角色特点、链路状态发生变化,STP如何重新收敛? TCN何时发?uplinkfast技术、Portfast技术、backbonefast技术、常见的STP调整命令:
  7. WCF 服务应用程序与 服务库之间的区别
  8. 500 Internal Server Error
  9. PDF文件保密和去除密码
  10. 最新BXP2006无盘教学办公系统
  11. kali 破解压缩包密码
  12. 计算机教室的英文音标,小学四年级英语单词(带音标).doc
  13. 有了雀巢智能咖啡机,单身狗离“秀恩爱”还会远吗?
  14. leetcode 1175. Prime Arrangements 解法 python
  15. w ndows无法连接到System,Windows无法连接到System Event Notification Service 服务
  16. 马虎词汇教程31-35(转载)
  17. 720P、1080P、4K之间有什么区别和联系?
  18. 深入分析JavaWeb Item44 -- Struts2开发核心之动作类Action
  19. C++中 pair 和 make_pair 的用法
  20. c语言整数编年历系统,程序员必知的编程语言编年史

热门文章

  1. php的数组排序方法,PHP 数组排序方法总结 推荐收藏
  2. php鼠标移动脚本,按键精灵鼠标移动脚本–eq
  3. 全国大学生网络安全精英赛练习题
  4. Linux测试常用命令
  5. 新版win10无法连接共享打印机的powershell解决方法
  6. 2020-2021中国门窗百强榜单正式公布
  7. android 短信 失败怎么办,手机短信发不出去怎么办 手机故障解决方法【方法步骤】...
  8. SVN 1.8.x 服务器安装
  9. neo4j 数组属性(属性值有多个)
  10. 字符串加减法(整数,小数)