通用对象池化框架Apache Commons Pool 2简析

前言

我们知道,创建Java对象会涉及到对象初始化、内存分配、类加载等多个步骤。当对象比较重量级时,频繁创建对象会带来可观的性能开销,所以在上古时代(大雾)就产生了对象池化(object pooling)的技术。顾名思义,对象池中维护了一批已经创建好的重量级对象,要使用时就从中取出一个,用完就归还到池里,通过复用对象来提高效率。

我们在日常工作中时刻都在应用池化技术:线程池、数据库连接池、网络连接池等等。自行实现对象池要考虑的细节太多,所以Apache Commons Pool提供了可以开箱即用的通用对象池实现,Jedis、DBCP等我们耳熟能详的组件都充分利用了它。本文择重点分析一下Commons Pool 2的设计思路。

对象池化的三要素

Commons Pool 2把面向接口编程的理念发挥得淋漓尽致,其三要素都可以很容易地从接口规范中看出来。下面分别讨论之。

池化对象

顾名思义,PooledObject就是对象池中存放着的对象。它除了维护对象本身之外,还会持有一些与对象生命周期相关的属性,如当前的状态、创建时间、空闲时间、最近借用/归还时间等等。对象的实际分配与销毁、空闲检测、状态转换等也由PooledObject来实现。接口代码如下,比较容易理解。

public interface PooledObject<T> extends Comparable<PooledObject<T>> {
    T getObject();

    long getCreateTime();
    long getActiveTimeMillis();

    default long getBorrowedCount() {
        return -1;
    }

    long getIdleTimeMillis();
    long getLastBorrowTime();
    long getLastReturnTime();
    long getLastUsedTime();

    @Override
    int compareTo(PooledObject<T> other);

    @Override
    boolean equals(Object obj);

    @Override
    int hashCode();

    @Override
    String toString();

    boolean startEvictionTest();
    boolean endEvictionTest(Deque<PooledObject<T>> idleQueue);

    boolean allocate();
    boolean deallocate();
    void invalidate();
    void setLogAbandoned(boolean logAbandoned);

    default void setRequireFullStackTrace(final boolean requireFullStackTrace) {
        // noop
    }

    void use();

    void printStackTrace(PrintWriter writer);

    PooledObjectState getState();

    void markAbandoned();
    void markReturning();
}

Commons Pool 2提供了两种PooledObject的实现,一是默认的DefaultPooledObject,二是基于软引用的PooledSoftReference,如下图所示。

对象池

对象池的作用自然是管理PooledObject。客户端可以通过对应的方法来借用或者归还PooledObject,另外也可以向池中添加或者从池中销毁PooledObject。普通ObjectPool使用一个池子管理所有的对象,而另外一种KeyedObjectPool则使用key标记的不同池子管理所有的对象(但仍然要求对象的类型相同)。篇幅限制,本文只讨论ObjectPool,该接口的源码如下。

public interface ObjectPool<T> extends Closeable {
    void addObject() throws Exception, IllegalStateException,
            UnsupportedOperationException;

    default void addObjects(final int count) throws Exception {
        for (int i = 0; i < count; i++) {
            addObject();
        }
    }

    T borrowObject() throws Exception, NoSuchElementException,
            IllegalStateException;

    void clear() throws Exception, UnsupportedOperationException;

    @Override
    void close();

    int getNumActive();
    int getNumIdle();

    void invalidateObject(T obj) throws Exception;

    void returnObject(T obj) throws Exception;
}

Commons Pool 2提供了3种ObjectPool的实现,分别是通用的GenericObjectPool、基于软引用的SoftReferenceObjectPool、基于动态代理的ProxiedObjectPool。本文接下来的分析主要关注GenericObjectPool。

池化对象工厂

是用于产生和控制池化对象的工厂类,是唯一一个需要完全由用户自行实现的组件。与对象池相对地,有负责普通ObjectPool的工厂PooledObjectFactory,以及负责KeyedObjectPool的KeyedPooledObjectFactory。PooledObjectFactory接口的源码如下。

public interface PooledObjectFactory<T> {
  PooledObject<T> makeObject() throws Exception;

  void destroyObject(PooledObject<T> p) throws Exception;

  boolean validateObject(PooledObject<T> p);

  void activateObject(PooledObject<T> p) throws Exception;

  void passivateObject(PooledObject<T> p) throws Exception;
}

上面的5个方法分别用来创建、销毁、校验、(在借用时)激活和(在归还时)钝化对象。

池化对象状态机

PooledObject的状态由PooledObjectState枚举来定义,一共有10种状态,列举如下。

  • IDLE:位于对象池的空闲队列中,未被使用。
  • ALLOCATED:正在使用。
  • EVICTION:位于空闲队列中,且正在做空闲检测,有可能会被驱逐。
  • EVICTION_RETURN_TO_HEAD:对象正在做空闲检测的同时被借用,此状态是一个中间状态,当空闲检测结束后,应该插回空闲队列。
  • VALIDATION:位于空闲队列中,且正在被校验。
  • VALIDATION_PREALLOCATED、VALIDATION_RETURN_TO_HEAD:对象正在校验的同时被借用,此两个状态都是中间状态,当校验结束后,应该插回空闲队列。不过,前者是在配置了testOnBorrow时出现,后者是在配置了空闲检测时出现。
  • INVALID:对象无效化(即没有通过空闲检测或者有效性校验),即将或已经被销毁。
  • ABANDONED:对象被标记为弃用,将被无效化。
  • RETURNING:对象使用完毕,正在被归还到池中。

根据默认实现DefaultPooledObject的逻辑(源码很简单,就不再贴了),可以画出其状态机如下图,每条边上都是DefaultPooledObject对应的方法。注意VALIDATION、VALIDATION_PREALLOCATED、VALIDATION_RETURN_TO_HEAD三个状态未被使用。

对象池参数简介

我们在之前使用Jedis、DBCP等连接池时,对对象池的相关参数应该有相当的了解,下面简要列举一些。

  • maxTotal:池化对象的最大数量。
  • maxIdle/minIdle:空闲对象的最大、最小数量。
  • lifo:空闲对象队列的出入队方式,可配置为后进先出(LIFO)和先进先出(FIFO)。
  • maxWaitMillis:借用对象时可以等待的最长时间。
  • blockWhenExhausted:当池中对象耗尽后,借用对象的操作是否阻塞。
  • testOnCreate/testOnBorrow/testOnReturn:创建/借用/归还对象时,是否校验对象的有效性。
  • testWhileIdle:是否校验空闲对象的有效性。
  • timeBetweenEvictionRunsMillis:空闲检测的周期。
  • numTestsPerEvictionRun:每次运行空闲检测时,最多被检测的空闲对象数量。
  • minEvictableIdleTimeMillis:空闲对象被回收掉的最小空闲时长。

在下文介绍对象借用和归还过程时,部分参数还会出现。

通用对象池GenericObjectPool实现要点

以下从对象的存储、创建、借用和归还四个方面作简要的分析。

对象存储

GenericObjectPool使用一个ConcurrentHashMap存储全部对象,保证线程安全性。

private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects = new ConcurrentHashMap<>();

注意IdentityWrapper只是简单地用System.identityHashCode()方法覆盖了默认的hashCode()实现,从而保证key的唯一性。

空闲队列则使用框架内自行实现的双端阻塞队列LinkedBlockingDeque。关于JDK中阻塞队列的经典实现,可以参见笔者之前写的关于LinkedBlockingQueue的文章

private final LinkedBlockingDeque<PooledObject<T>> idleObjects;

上文讲配置参数时已经说过,空闲队列是可以配置FIFO和LIFO两种出入队方式的,在队头和队尾都能插入元素,所以双端队列是必要的。

创建池化对象

create()方法比较简单,就是调用了PooledObjectFactory.makeObject()方法,将其加入ConcurrentHashMap中,并且保证总对象数不超过maxTotal的限制。

private PooledObject<T> create() throws Exception {
    int localMaxTotal = getMaxTotal();
    long newCreateCount = createCount.incrementAndGet();
    if (localMaxTotal > -1 && newCreateCount > localMaxTotal || newCreateCount > Integer.MAX_VALUE) {
        createCount.decrementAndGet();
        return null;
    }

    final PooledObject<T> p;
    try {
        p = factory.makeObject();
    } catch (Exception e) {
        createCount.decrementAndGet();
        throw e;
    }

    AbandonedConfig ac = this.abandonedConfig;
    if (ac != null && ac.getLogAbandoned()) {
        p.setLogAbandoned(true);
    }

    createdCount.incrementAndGet();
    allObjects.put(p.getObject(), p);
    return p;
}

借用对象

borrowObject()方法的完整源码如下。

public T borrowObject(long borrowMaxWaitMillis) throws Exception {
    assertOpen();
    AbandonedConfig ac = this.abandonedConfig;
    if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
            (getNumIdle() < 2) &&
            (getNumActive() > getMaxTotal() - 3) ) {
        removeAbandoned(ac);
    }

    PooledObject<T> p = null;
    // Get local copy of current config so it is consistent for entire
    // method execution
    boolean blockWhenExhausted = getBlockWhenExhausted();
    boolean create;
    long waitTime = System.currentTimeMillis();

    while (p == null) {
        create = false;
        if (blockWhenExhausted) {
            p = idleObjects.pollFirst();
            if (p == null) {
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (p == null) {
                if (borrowMaxWaitMillis < 0) {
                    p = idleObjects.takeFirst();
                } else {
                    p = idleObjects.pollFirst(borrowMaxWaitMillis,
                            TimeUnit.MILLISECONDS);
                }
            }
            if (p == null) {
                throw new NoSuchElementException(
                        "Timeout waiting for idle object");
            }
            if (!p.allocate()) {
                p = null;
            }
        } else {
            p = idleObjects.pollFirst();
            if (p == null) {
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            if (p == null) {
                throw new NoSuchElementException("Pool exhausted");
            }
            if (!p.allocate()) {
                p = null;
            }
        }

        if (p != null) {
            try {
                factory.activateObject(p);
            } catch (Exception e) {
                try {
                    destroy(p);
                } catch (Exception e1) {
                    // Ignore - activation failure is more important
                }
                p = null;
                if (create) {
                    NoSuchElementException nsee = new NoSuchElementException(
                            "Unable to activate object");
                    nsee.initCause(e);
                    throw nsee;
                }
            }
            if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                boolean validate = false;
                Throwable validationThrowable = null;
                try {
                    validate = factory.validateObject(p);
                } catch (Throwable t) {
                    PoolUtils.checkRethrow(t);
                    validationThrowable = t;
                }
                if (!validate) {
                    try {
                        destroy(p);
                        destroyedByBorrowValidationCount.incrementAndGet();
                    } catch (Exception e) {
                        // Ignore - validation failure is more important
                    }
                    p = null;
                    if (create) {
                        NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to validate object");
                        nsee.initCause(validationThrowable);
                        throw nsee;
                    }
                }
            }
        }
    }

    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
    return p.getObject();
}

代码很长,但是思路也非常清晰。简要叙述流程:

  1. 确认对象池是打开的,并根据AbandonedConfig的条件清理要被丢弃的对象(具体逻辑略去)。
  2. 如果blockWhenExhausted参数为true,就以阻塞的方式从空闲队列中获取对象,获取不到则创建。如果仍然无法得到对象,就根据maxWaitMillis的设定,再次以阻塞方式从空闲队列中获取对象,超时则抛出异常。
  3. 如果blockWhenExhausted参数为false,就以非阻塞的方式从空闲队列中获取对象,获取不到则创建。如果仍然无法得到对象,直接抛出异常。
  4. 得到可用的池化对象之后,将其激活。如果配置了testOnCreate或者testOnBorrow参数,则还要进行校验。激活或者校验不通过都会将对象销毁。
  5. 返回池化对象。

归还对象

returnObject()的完整源码如下。

public void returnObject(T obj) {
    PooledObject<T> p = allObjects.get(new IdentityWrapper<T>(obj));
    
    if (p == null) {
        if (!isAbandonedConfig()) {
            throw new IllegalStateException(
                    "Returned object not currently part of this pool");
        } else {
            return; // Object was abandoned and removed
        }
    }

    synchronized(p) {
        final PooledObjectState state = p.getState();
        if (state != PooledObjectState.ALLOCATED) {
            throw new IllegalStateException(
                    "Object has already been returned to this pool or is invalid");
        } else {
            p.markReturning(); // Keep from being marked abandoned
        }
    }

    long activeTime = p.getActiveTimeMillis();
    if (getTestOnReturn()) {
        if (!factory.validateObject(p)) {
            try {
                destroy(p);
            } catch (Exception e) {
                swallowException(e);
            }
            try {
                ensureIdle(1, false);
            } catch (Exception e) {
                swallowException(e);
            }
            updateStatsReturn(activeTime);
            return;
        }
    }

    try {
        factory.passivateObject(p);
    } catch (Exception e1) {
        swallowException(e1);
        try {
            destroy(p);
        } catch (Exception e) {
            swallowException(e);
        }
        try {
            ensureIdle(1, false);
        } catch (Exception e) {
            swallowException(e);
        }
        updateStatsReturn(activeTime);
        return;
    }
    if (!p.deallocate()) {
        throw new IllegalStateException(
                "Object has already been returned to this pool or is invalid");
    }

    int maxIdleSave = getMaxIdle();
    if (isClosed() || maxIdleSave > -1 && maxIdleSave <= idleObjects.size()) {
        try {
            destroy(p);
        } catch (Exception e) {
            swallowException(e);
        }
    } else {
        if (getLifo()) {
            idleObjects.addFirst(p);
        } else {
            idleObjects.addLast(p);
        }
        if (isClosed()) {
            // Pool closed while object was being added to idle objects.
            // Make sure the returned object is destroyed rather than left
            // in the idle object pool (which would effectively be a leak)
            clear();
        }
    }
    updateStatsReturn(activeTime);
}

简要叙述流程:

  1. 获取原始对象对应的池化对象实例。如果实例为空且配置了AbandonedConfig,说明已经被丢弃,无需再归还。
  2. 检查池化对象的状态,只有处于ALLOCATED状态才能被归还。
  3. 如果配置了testOnReturn参数,则校验对象的有效性,不能通过校验则直接销毁。通过校验之后,再钝化与解分配此对象。
  4. 检查当前空闲队列中的对象数量是否达到了maxIdle的阈值,若达到阈值,说明无法再归还,直接销毁。
  5. 根据空闲队列的LIFO/FIFO方式,将被归还的对象放到队列的头部或尾部。

The End

关于连接池的借用、归还和空闲检测,之前已经简单提到过了,参见MySQL连接的8小时问题

民那晚安。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351