一文搞懂Java中對象池的實現

最近在分析一個應用中的某個接口的耗時情況時,發現一個看起來極其普通的對象創建操作,竟然每次需要消耗 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> {

    @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 類的繼承關系圖也可以發現。

JedisPool 繼承關系

6. 對象池總結

通過這篇文章的介紹,可以發現池化思想有幾個明顯的優勢。

  • 1. 可以顯著的提高應用程序的性能。
  • 2. 如果一個對象創建成本過高,那麼使用池化非常有效。
  • 3. 池化提供瞭一種對象的管理以及重復使用的方式,減少內存碎片。
  • 4. 可以為對象的創建數量提供限制,對某些對象不能創建過多的場景提供保護。

但是使用對象池化也有一些需要註意的地方,比如歸還對象時應確保對象已經被重置為可以重復使用的狀態。同時也要註意,使用池化時要根據具體的場景合理的設置池子的大小,過小達不到想要的效果,過大會造成內存浪費。

以上就是一文搞懂Java中對象池的實現的詳細內容,更多關於Java對象池的資料請關註WalkonNet其它相關文章!

推薦閱讀: