广告
返回顶部
首页 > 资讯 > 后端开发 > Python >一文搞懂Java中对象池的实现
  • 781
分享到

一文搞懂Java中对象池的实现

2024-04-02 19:04:59 781人浏览 薄情痞子

Python 官方文档:入门教程 => 点击学习

摘要

目录1. 什么是对象池2. 为什么需要对象池3. 对象池的实现4. 开源的对象池工具5. JedisPool 对象池实现分析6. 对象池总结最近在分析一个应用中的某个接口的耗时情况时

最近在分析一个应用中的某个接口的耗时情况时,发现一个看起来极其普通的对象创建操作,竟然每次需要消耗 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;


public class MyObjectPool<T extends Closeable> {

    // 池子大小
    private Integer size = 5;
    // 对象池栈。后进先出
    private Stack<T> stackPool = new Stack<>();
    // 借出的对象的 hashCode 集合
    private HashSet<Integer> borrowHashCodeSet = new HashSet<>();

    
    public synchronized void addObj(T t) {
        if ((stackPool.size() + borrowHashCodeSet.size()) == size) {
            throw new RuntimeException("池中对象已经达到最大值");
        }
        stackPool.add(t);
        System.out.println("添加了对象:" + t.hashCode());
    }

    
    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;
    }

    
    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;


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> {

    @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> {

    
    public static final int DEFAULT_MAX_TOTAL = 8;

    
    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;


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;


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 类的继承关系图也可以发现。

JedisPool 继承关系

6. 对象池总结

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

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

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

以上就是一文搞懂Java中对象池的实现的详细内容,更多关于Java对象池的资料请关注编程网其它相关文章!

--结束END--

本文标题: 一文搞懂Java中对象池的实现

本文链接: https://www.lsjlt.com/news/154083.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 一文搞懂Java中对象池的实现
    目录1. 什么是对象池2. 为什么需要对象池3. 对象池的实现4. 开源的对象池工具5. JedisPool 对象池实现分析6. 对象池总结最近在分析一个应用中的某个接口的耗时情况时...
    99+
    2022-11-13
  • 一篇文章带你搞懂Java线程池实现原理
    目录1. 为什么要使用线程池2. 线程池的使用3. 线程池核心参数4. 线程池工作原理5. 线程池源码剖析5.1 线程池的属性5.2 线程池状态5.3 execute源码5.4 wo...
    99+
    2022-11-13
    Java线程池实现原理 Java线程池原理 Java线程池实现 Java线程池
  • 一文读懂java中的面向对象
    这期内容当中小编将会给大家带来有关一文读懂java中的面向对象,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。无论面向对象还是面向过程, 这俩都是解决问题的思路而已, 只是角度不同.面向过程:强调解决问题的...
    99+
    2023-05-31
    面向对象 java ava
  • 一文带你搞懂Java中Object类和抽象类
    目录一、抽象类是什么二、初始抽象类2.1 基本语法2.2 继承抽象类三、抽象类总结四、Object类4.1 初始Object4.2 toString4.3 equals4.4 has...
    99+
    2022-11-13
  • 一文搞懂Java中的日期类
    目录一、日期类1.1 第一代日期类1.2 第二代日期类Calendar1.3 第三代日期类一、日期类 在程序的开发中我们经常会遇到日期类型的操作,Java对日期类型的操作提供了很好的...
    99+
    2022-11-13
  • 【C++漂流记】一文搞懂类与对象的封装
    本篇文章主要说明了类与对象中封装的有关知识,包括属性和行为作为整体、访问权限、class与struct的区别、成员属性的私有化,希望这篇文章可以帮助你更好的了解类与对象这方面的知识。 ...
    99+
    2023-09-17
    c++ 前端 开发语言
  • 一文搞懂JavaScript中bind,apply,call的实现
    目录bind、call和apply的用法bindcall&apply实现bind实现call和apply总结bind、call和apply都是Function原型链上面的方法...
    99+
    2022-11-13
  • 一文带你搞懂Java中的递归
    目录概述递归累加求和计算1 ~ n的和代码执行图解递归求阶乘递归打印多级目录综合案例文件搜索文件过滤器优化Lambda优化概述 递归:指在当前方法内调用自己的这种现象。 递归的分类:...
    99+
    2022-11-13
    Java 递归
  • 一文搞懂Java中的反射机制
    目录一. 反射的概念二. 为什么需要反射三. 反射的基石四. 反射的实现1. 获取字节码文件对象 2. 反射的使用 反射的优缺点 一. 反射的概念 Ja...
    99+
    2022-11-13
  • 一文搞懂PHP中的抽象类和接口
    目录一.抽象类1.定义2.应用场景3.入门代码4.细节5. 代码分解二.接口1.定义2. 应用场景3.入门代码4.注意细节三.类和接口之间关系一.抽象类 1.定义...
    99+
    2022-11-13
  • 一文带你读懂java中的对象拷贝
    本篇文章为大家展示了一文带你读懂java中的对象拷贝,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。java对象拷贝详解及实例Java赋值是复制对象引用,如果我们想要得到一个对象的副本,使用赋值操作是...
    99+
    2023-05-31
    java 对象拷贝 ava
  • 一文搞懂Java中的注解和反射
    目录1、注解(Annotation)1.1 什么是注解(Annotation)1.2 内置注解1.3 元注解(meta-annotation)1.4 自定义注解2、反射(Reflec...
    99+
    2022-11-13
  • 一文搞懂vue中provide和inject实现原理对抗平庸
    目录前言provide & inject 的用法下面例子需要实现什么内心中的呈现下面例子跨层级查找inject放下伪装,吐露心声总结前言 在没有面试又似乎马上就会有面试的躁...
    99+
    2023-05-16
    vue provide inject实现原理 vue provide inject
  • 一文搞懂Java中的抽象类和接口到底是什么
    目录什么是抽象类抽象类在实现多态中的意义接口是什么通过接口实现多态今天让我们继续学习java,看看java中的抽象类和接口到底是什么? 什么是抽象类 我们之前学过什么是类,那么抽象...
    99+
    2022-11-13
  • 一文搞懂Java JDBC中的SQL注入问题
    目录SQL注入✨什么是SQL注入✨SQL注入的效果的演示SQL注入代码SQL注入效果✨如何避免SQL注入PrepareStatement解决SQL注入✨PreparedStateme...
    99+
    2022-11-13
    jdbc sql 注入
  • 一文搞懂C#实现读写文本文件中的数据
    【1】首先我们定义一段假数据,这里以一个string为例字   static void Main(string[] args) { string data = "我的数据要开始...
    99+
    2022-11-12
  • 一文搞懂JavaMD5算法的原理及实现
    目录MD5加密简介MD5加密原理MD5加密常用方法MD5加密简介 哈希算法又称散列算法,是将任何数据转换成固定长度的算法的统称。 从本质上讲,MD5也是一种哈希算法,其输出...
    99+
    2022-11-13
  • 一文读懂C++中的函数对象
    这篇文章给大家介绍一文读懂C++中的函数对象,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。如果一个类将()运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象。函数对象是一个对象,但是使用的形式看起来...
    99+
    2023-06-06
  • Java中对象池怎么实现
    这篇“Java中对象池怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Java中对象池怎么实现”文章吧。1. 什么是对...
    99+
    2023-07-02
  • 搞懂Java分布式锁实现看这篇文章就对了
     前言:随着微处理机技术的发展,人们只需花几百美元就能买到一个CPU芯片,这个芯片每秒钟执行的指令比80年代最大的大型机的处理机每秒钟所执行的指令还多。如果你愿意付出两倍的价钱,将得到同样的CPU,但它却以更高的时钟速率运行。因此...
    99+
    2023-06-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作