ホームページ >Java >&#&チュートリアル >Javaでオブジェクトプールを実装する方法

Javaでオブジェクトプールを実装する方法

WBOY
WBOY転載
2023-05-11 14:49:061335ブラウズ

1. オブジェクト プールとは

プーリングは新しいテクノロジではありません。むしろソフトウェア設計パターンに似ています。その主な機能は、初期化されたオブジェクトのセットをキャッシュして、いつでも使用できるようにすることです。時間。ほとんどのシナリオでは、オブジェクト プールには、作成するにはコストが高すぎるか、作成して繰り返し使用する必要があるオブジェクトがキャッシュされます。プールからオブジェクトを取得する時間は予測可能ですが、新しいオブジェクトを作成する時間は不確かです。

新しいオブジェクトが必要な場合、プールから 1 つが借用され、オブジェクト プールは現在のオブジェクトが使用中であることをマークします。使用後は、貸与できるようにオブジェクト プールに戻されます。また外へ。

オブジェクト プーリングを使用する一般的なシナリオ:

  • 1. オブジェクト作成のコストが高すぎます。

  • 2. 多数の重複オブジェクトを頻繁に作成すると、大量のメモリの断片化が発生します。

  • 3. 同時に使用されるオブジェクトが多すぎることはありません。

  • 4. データベース接続プール、スレッド プールなどの一般的な特定のシナリオ。

2. オブジェクト プールが必要な理由

たとえば、オブジェクトの作成コストが非常に高い場合、データベース接続を確立します。プーリング テクノロジを使用しない場合、クエリ プロセスは次のようになります。

  • クエリ 1: データベース接続の確立 -> クエリの開始 -> 応答の受信 -> 接続の終了

  • クエリ 2: データベースの確立接続 -> クエリの開始 -> 応答の受信 -> 接続の終了

  • クエリ 3: データベース接続の確立 -> クエリの開始 -> 応答の受信 -> 接続を閉じる

このモードでは、閉じられた接続をクエリごとに再確立する必要があります。接続の確立には時間のかかる操作であるため、このモードはプログラムの全体的なパフォーマンスに影響します。 。

それでは、プーリング思考を使用するとどうなるでしょうか?同じプロセスが次のステップに変換されます。

  • 初期化: N 個のデータベース接続を確立します -> キャッシュします

  • クエリ 1: キャッシュからデータベース接続を借用します -> クエリを開始します -> ; 応答を受信 -> データベース接続オブジェクトをキャッシュに返す

  • クエリ 2: キャッシュからデータベース接続を借用 -> クエリを開始 -> 応答を受信 -> データベース接続オブジェクトを返すキャッシュへ

    ##クエリ 3: キャッシュからデータベース接続を借用します -> クエリを開始します -> 応答を受信します -> データベース接続オブジェクトをキャッシュに返します
  • プーリングのアイデアを使用した後は、データベース接続が頻繁に作成および閉じられることはなくなります。代わりに、N 個の接続が起動後にその後の使用のために初期化され、使用後にオブジェクトが返されます。このようにして、プログラム全体のパフォーマンスが向上します。
3. オブジェクト プールの実装

上記の例を通じて、プーリングのアイデアのいくつかの重要なステップ (

初期化、貸出、返却

) もわかります。

destruction の手順は上には示されていませんが、シナリオによっては、接続の解放など、オブジェクトの破棄プロセスも必要になります。 以下では、オブジェクト プールについての理解を深めるために、単純なオブジェクト プールを手動で実装します。主なことは、オブジェクト プール管理クラスを定義し、その中でオブジェクトの 初期化、貸し出し、返却、破棄、およびその他の操作を実装することです。

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 接続を通常に作成し、クエリを実行して接続を閉じるパフォーマンスを比較すると、この 2 つのパフォーマンスが大きく異なることがわかります。以下にテスト結果を示しますが、オブジェクトプーリングを使用した後のパフォーマンスは、プーリングを使用しない方法に比べて約 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はyisu.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。