点赞再看,能源有限。 微信搜「 程序猿阿朗 」。

本文 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借出了对象: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 依赖:

<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> {    @Override    public void activateObject(PooledObject<Jedis> pooledObject) throws Exception {    }    @Override    public void destroyObject(PooledObject<Jedis> pooledObject) throws Exception {        Jedis jedis = pooledObject.getObject();        jedis.close();          System.out.println("开释连贯");    }    @Override    public PooledObject<Jedis> makeObject() throws Exception {        return new DefaultPooledObject(new Jedis("localhost", 6379));    }    @Override    public void passivateObject(PooledObject<Jedis> pooledObject) throws Exception {    }    @Override    public 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> {    @Override    public 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;    }    @Override    public 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.Poolpublic 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.Poolpublic 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。

<img width="400px" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c2baa841489f4e659c8cd039deffd797~tplv-k3u1fbpfcp-zoom-1.image">