对象池模式

一个对象池包含一组已经初始化过且可以使用的对象,而可以在有需求时创建和销毁对象。池的用户可以从池子中取得对象,对其进行操作处理,并在不需要时归还给池子而非直接销毁它。这是一种特殊的工厂对象。

优点

复用池中对象,消除创建对象、回收对象 所产生的内存开销、cpu开销以及(若跨网络)产生的网络开销。

常见的使用对象池有:在使用socket时(包括各种连接池)、线程、数据库连接池等。

缺点

  1. 现在Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计;
  2. 并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
  3. 由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
  4. 很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高。

场景

  1. 资源受限的, 不需要可伸缩性的环境(cpu\内存等物理资源有限): cpu性能不够强劲, 内存比较紧张, 垃圾收集, 内存抖动会造成比较大的影响, 需要提高内存管理效率, 响应性比吞吐量更为重要;
  2. 数量受限的, 比如数据库连接;
  3. 创建成本高的对象, 可以考虑是否池化, 比较常见的有线程池(ThreadPoolExecutor), 字节数组池等。

使用

Apache 提供了一个通用的对象池技术的实现: Common Pool2,可以很方便的实现自己需要的对象池,而不需要自己实现一个对象池。

核心接口

  • ObjectPool:对象池,持有对象并提供取/还等方法;
  • PooledObjectFactory:对象工厂,提供对象的创建、初始化、销毁等操作,由 Pool 调用。一般需要使用者自己实现这些操作;
  • PooledObject:池化对象,对池中对象的封装,封装对象的状态和一些其他信息。
image

Common Pool2 提供的最基本的实现就是由 Factory 创建对象并使用PooledObject 封装对象放入 Pool 中。

对象池实现

image

对象池有两个基础的接口 ObjectPoolKeyedObjectPool, 持有的对象都是由 PooledObject 封装的池化对象。 KeyedObjectPool 的区别在于其是用键值对的方式维护对象。

ObjectPoolKeyedObjectPool 分别有一个默认的实现类GenericObjectPoolGenericKeyedObjectPool 可以直接使用,他们的公共部分和配置被抽取到了 BaseGenericObjectPool 中。

SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个SoftReference中。SoftReference允许垃圾回收机制在需要释放内存时回收对象池中的对象,可以避免一些内存泄露的问题。

ObjectPool

public interface ObjectPool<T> {
    // 从池中获取一个对象,客户端在使用完对象后必须使用 returnObject 方法返还获取的对象
    T borrowObject() throws Exception, NoSuchElementException,
            IllegalStateException;

    // 将对象返还到池中。对象必须是从 borrowObject 方法获取到的
    void returnObject(T obj) throws Exception;

    // 使池中的对象失效,当获取到的对象被确定无效时(由于异常或其他问题),应该调用该方法
    void invalidateObject(T obj) throws Exception;

    // 池中当前闲置的对象数量
    int getNumIdle();

    // 当前从池中借出的对象的数量
    int getNumActive();

    // 清除池中闲置的对象
    void clear() throws Exception, UnsupportedOperationException;

    // 关闭这个池,并释放与之相关的资源
    void close();

    ...
}

PooledObjectFactory

对象工厂,负责对象的创建、初始化、销毁和验证等工作。Factory 对象由ObjectPool持有并使用

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

Common Pool2 并没有提供 PooledObjectFactory 可以直接使用的子类实现,因为对象的创建、初始化、销毁和验证的工作无法通用化,需要由使用方自己实现。不过它提供了一个抽象子类 BasePooledObjectFactory,实现自己的工厂时可以继承BasePooledObjectFactory,就只需要实现 createwrap 两个方法了。

PooledObject

public interface PooledObject<T> extends Comparable<PooledObject<T>> {
    // 获取封装的对象
    T getObject();

    // 对象创建的时间
    long getCreateTime();

    // 对象上次处于活动状态的时间
    long getActiveTimeMillis();

    // 对象上次处于空闲状态的时间
    long getIdleTimeMillis();

    // 对象上次被借出的时间
    long getLastBorrowTime();

    // 对象上次返还的时间
    long getLastReturnTime();

    // 对象上次使用的时间
    long getLastUsedTime();

    // 将状态置为 PooledObjectState.INVALID
    void invalidate();

    // 更新 lastUseTime
    void use();

    // 获取对象状态
    PooledObjectState getState();

    // 将状态置为 PooledObjectState.ABANDONED
    void markAbandoned();

    // 将状态置为 PooledObjectState.RETURNING
    void markReturning();
}

对象池配置

对象池配置提供了对象池初始化所需要的参数,Common Pool2 中的基础配置类是BaseObjectPoolConfig。其有两个实现类分别为 GenericObjectPoolConfigGenericKeyedObjectPoolConfig,分别为 GenericObjectPoolGenericKeyedObjectPool 所使用。

下面是一些重要的配置项:

  • lifo 连接池放池对象的方式,true:放在空闲队列最前面,false:放在空闲队列最后面,默认为 true
  • fairness 从池中获取/返还对象时是否使用公平锁机制,默认为 false
  • maxWaitMillis 获取资源的等待时间。blockWhenExhausted 为 true 时有效。-1 代表无时间限制,一直阻塞直到有可用的资源
  • minEvictableIdleTimeMillis 对象空闲的最小时间,达到此值后空闲对象将可能会被移除。-1 表示不移除;默认 30 分钟
  • softMinEvictableIdleTimeMillis 同上,额外的条件是池中至少保留有 minIdle 所指定的个数的对象
  • numTestsPerEvictionRun 资源回收线程执行一次回收操作,回收资源的数量。默认 3
  • evictionPolicyClassName 资源回收策略,默认值 org.apache.commons.pool2.impl.DefaultEvictionPolicy
  • testOnCreate 创建对象时是否调用 factory.validateObject 方法,默认 false
  • testOnBorrow 取对象时是否调用 factory.validateObject 方法,默认 false
  • testOnReturn 返还对象时是否调用 factory.validateObject 方法,默认 false
  • testWhileIdle 池中的闲置对象是否由逐出器验证。无法验证的对象将从池中删除销毁。默认 false
  • timeBetweenEvictionRunsMillis 回收资源线程的执行周期,默认 -1 表示不启用回收资源线程
  • blockWhenExhausted 资源耗尽时,是否阻塞等待获取资源,默认 true

池化对象的状态

池化对象的状态定义在 PooledObjectState 枚举中,有以下值:

  • IDLE 在池中,处于空闲状态
  • ALLOCATED 被使用中
  • EVICTION 正在被逐出器验证
  • VALIDATION 正在验证
  • INVALID 驱逐测试或验证失败并将被销毁
  • ABANDONED 对象被客户端拿出后,长时间未返回池中,或没有调用 use 方法,即被标记为抛弃的

例子

// 资源类
public class Resource {

    private static int id;
    private int rid;

    public Resource() {
        synchronized (this) {
            this.rid = id++;
        }
    }

    public int getRid() {
        return this.rid;
    }

    @Override
    public String toString() {
        return "id:" + this.rid;
    }

}
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;

// 工厂类
public class ResourcePoolableObjectFactory extends BasePooledObjectFactory<Resource>{
    
    /**
     * 创建一个对象实例
     */
    @Override
    public Resource create() throws Exception {
        return new Resource();
    }
    
    /**
     * 包裹创建的对象实例,返回一个pooledobject
     */
    @Override
    public PooledObject<Resource> wrap(Resource obj) {
        return new DefaultPooledObject<Resource>(obj);
    }
    
}
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

public class Test {

    public static void main(String[] args) {
        // 创建池对象工厂
        PooledObjectFactory<Resource> factory = new ResourcePoolableObjectFactory();
        
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        // 最大空闲数
        poolConfig.setMaxIdle(5);
        // 最小空闲数, 池中只有一个空闲对象的时候,池会在创建一个对象,并借出一个对象,从而保证池中最小空闲数为1
        poolConfig.setMinIdle(1);
        // 最大池对象总数
        poolConfig.setMaxTotal(20);
        // 逐出连接的最小空闲时间 默认1800000毫秒(30分钟)
        poolConfig.setMinEvictableIdleTimeMillis(1800000);
        // 逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
        poolConfig.setTimeBetweenEvictionRunsMillis(1800000 * 2L);
        // 在获取对象的时候检查有效性, 默认false
        poolConfig.setTestOnBorrow(true);
        // 在归还对象的时候检查有效性, 默认false
        poolConfig.setTestOnReturn(false);
        // 在空闲时检查有效性, 默认false
        poolConfig.setTestWhileIdle(false);
        // 最大等待时间, 默认的值为-1,表示无限等待。
        poolConfig.setMaxWaitMillis(5000);
        // 是否启用后进先出, 默认true
        poolConfig.setLifo(true);
        // 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true
        poolConfig.setBlockWhenExhausted(true);
        // 每次逐出检查时 逐出的最大数目 默认3
        poolConfig.setNumTestsPerEvictionRun(3);
        
        // 创建对象池
        final GenericObjectPool<Resource> pool = new GenericObjectPool<Resource>(factory, poolConfig);  
        
        for (int i = 0; i < 40; i++) {  
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    try {  
                        Resource resource = pool.borrowObject();// 注意,如果对象池没有空余的对象,那么这里会block,可以设置block的超时时间  
                        System.out.println(resource);  
                        Thread.sleep(1000);  
                        pool.returnObject(resource);// 申请的资源用完了记得归还,不然其他人要申请时可能就没有资源用了  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                    }  
                }  
            }).start();  
        }  
    }
    
}

参考

一个广为人知但鲜有人用的技巧:对象池
Apache Common Pool2 对象池应用浅析

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