>  기사  >  Java  >  Java에서 객체 풀을 구현하는 방법

Java에서 객체 풀을 구현하는 방법

WBOY
WBOY앞으로
2023-05-11 14:49:061281검색

1. 개체 풀이란 무엇입니까

풀링은 새로운 기술이 아닙니다. 이는 소프트웨어 설계 패턴에 더 가깝습니다. 주요 기능은 초기화된 개체 집합을 캐시하여 언제든지 사용할 수 있도록 하는 것입니다. 대부분의 시나리오에서 개체 풀은 생성 비용이 너무 높거나 반복적으로 생성 및 사용해야 하는 개체를 캐시합니다. 풀에서 개체를 검색하는 데 걸리는 시간은 예측할 수 있지만 새 개체를 만드는 시간은 불확실합니다.

새로운 객체가 필요할 때 풀에서 빌려오며, 사용 후 다시 대여할 수 있도록 객체 풀에서 현재 객체가 사용 중임을 표시합니다.

객체 풀링 사용에 대한 일반적인 시나리오:

  • 1. 객체 생성 비용이 너무 높습니다.

  • 2. 다수의 중복 개체를 자주 생성하면 메모리 조각화가 많이 발생합니다.

  • 3. 동시에 너무 많은 물건을 사용하지 않습니다.

  • 4. 데이터베이스 연결 풀, 스레드 풀 등과 같은 일반적인 특정 시나리오

2. 객체 풀이 필요한 이유

예를 들어 객체 생성 비용이 매우 높은 경우 풀링 기술을 사용하지 않고 데이터베이스 연결을 설정하는 데 너무 오랜 시간이 걸리면 쿼리 프로세스가 그런.

  • 쿼리 1: 데이터베이스 연결 설정 -> 쿼리 시작 -> 연결 종료

  • 쿼리 2: 데이터베이스 연결 시작 -> 연결

  • 데이터베이스 연결 설정 -> 쿼리 시작 -> 응답 수신 -> 연결 닫기

이 모드에서는 각 쿼리에 대해 닫힌 연결을 다시 설정해야 합니다. 설정 결합은 시간이 많이 걸리는 작업이므로 이 모드는 프로그램의 전반적인 성능에 영향을 미칩니다.

그렇다면 풀링 아이디어를 활용하는 것은 어떤가요? 동일한 프로세스가 다음 단계로 변환됩니다.

  • 초기화: N개의 데이터베이스 연결 설정 -> 캐시

  • 쿼리 1: 캐시에서 데이터베이스 연결 대여 -> 쿼리 시작 -> 캐시

  • 쿼리 2: 캐시에서 데이터베이스 연결 대여 -> 쿼리 시작 -> 응답 수신 -> 데이터베이스 연결 개체를 캐시로 반환

  • 쿼리 3: 데이터베이스 연결 대여 캐시에서 -> 쿼리 시작 -> 응답 수신 -> 데이터베이스 연결 개체를 캐시로 반환

풀링 아이디어를 사용한 후에는 데이터베이스 연결이 자주 생성 및 종료되지 않습니다. 이후 사용을 위해 시작 후 초기화됩니다. 프로그램의 전반적인 성능이 향상되도록 사용 후 개체를 반환합니다.

3. 객체 풀 구현

위의 예를 통해 풀링 아이디어의 몇 가지 주요 단계인 초기화, 대여 및 반환을 찾을 수도 있습니다. destroy 단계는 위에 표시되지 않습니다. 일부 시나리오에서는 연결 해제와 같은 개체 삭제 프로세스도 필요합니다.

이제 객체 풀에 대한 이해를 심화하기 위해 간단한 객체 풀을 수동으로 구현해 보겠습니다. 가장 중요한 것은 객체 풀 관리 클래스를 정의한 후 그 안에 객체 초기화, 대여, 반납, 파기 등의 작업을 구현하는 것입니다.

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

Lent 개체: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> {

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

Java에서 객체 풀을 구현하는 방법

위 내용은 Java에서 객체 풀을 구현하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제