在应用开发中,在某些场景下,需要频繁重复创建某种对象来传递消息或者执行操作,在使用完之后又随即废弃。在这种情况下,会对系统性能造成一定的影响,特别是存在以下特征时:
- 对象频繁创建但是只需要短暂使用
- 对象的创建代价比较高。
比较典型的例子比如 Android 里面的 MotionEvent, 每次触发屏幕移动事件都会产生一个新的 MotionEvent 对象,但是这个 MotionEvent 对象却只使用一次就废弃了。
再比如数据库连接,连接数据库是非常耗时的,如果每次查询都创建一个连接,会产生显著的响应延迟。
在以上类似的场景中,我们引入“对象池化”技术,特别是在移动设备这种硬件资源紧张的系统中,池化技术更是无处不在。
对象池是一个保存可复用对象的容器,用户可以从池子中取得对象,对其进行操作处理,并在使用完毕之后将这个对象归还给池子,而不是任由系统销毁这个对象。我们简略区分一下,Java 对象的生命周期大致包括三个阶段:
- 对象的创建
- 对象的使用
- 对象的销毁
池化通过减少“对象的创建”和”对象的销毁”这两步的开销,进而降低系统负担从而减少了响应时间,是一种以空间换时间的策略。
不过同时,池化也引入了新的风险,既然池中的对象是复用的,那么这些对象先前的状态如何处理。此时,又需要将池化对象分两种:
- 无状态对象。这种对象可以直接复用,因为每次使用的状态都是一样的。
- 有状态对象。这种对象在取出(或者归还)的时候,都应该先将所有的对象重置,所以你在很多对象上能看到 recycle() 方法,这个对象很大程度上有可能就使用了池化技术。
对象池的实现
对象池的实现有很多,最基本的只需要实现“取出”和“归还”功能即可,一个简单的实现:
1 2 3 4
| interface Pool<T> { T acquire() boolean release(T instance) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public static class SimplePool<T> implements Pool<T> { private final Object[] mPool;
private int mPoolSize;
public SimplePool(int maxPoolSize) { mPool = new Object[maxPoolSize]; }
@Override @SuppressWarnings("unchecked") public T acquire() { if (mPoolSize > 0) { final int lastPooledIndex = mPoolSize - 1; T instance = (T) mPool[lastPooledIndex]; mPool[lastPooledIndex] = null; mPoolSize--; return instance; } return null; }
@Override public boolean release(T instance) { if (isInPool(instance)) { throw new IllegalStateException("Already in the pool!"); } if (mPoolSize < mPool.length) { mPool[mPoolSize] = instance; mPoolSize++; return true; } return false; }
private boolean isInPool(T instance) { for (int i = 0; i < mPoolSize; i++) { if (mPool[i] == instance) { return true; } } return false; } }
|
上面其实是 Android v4 包里面的实现,整体就是用一个数组来保存复用对象,然后依次轮询。
什么时候不适合池化?
- 过于轻量级对象不适合池化,因为创建的开销可能比在池中取出归还的消耗还低。不过这是一个很主观的标准,在 android 这种移动系统中,每一次创建对象都要在堆上进行分配,每一次销毁都要等待垃圾回收处理,就算单次创建的消耗低,次数多了一样会拖累系统。
- 对象创建之后,每次使用都会持有较长时间,此时相比“对象的使用”的时间来说,“对象的创建”和“对象的销毁”的开销基本可以忽略了,创建一次对象也算是物超所值了。同时,长时间的持有对象能造成两种结果:1. 对象池越来越大 2. 客户无法从对象池获取到对象了,因为都在使用中。不过这同样也是一个很主观的标准,比如,线程池就是一个很好的例子。你可以对每一个用户请求都新开一个线程来处理,但是当达到负载峰值时,根本就不可能为每一个用户请求创建一个线程,因此,用户请求只能排队等候了。
- 对象的状态难以重置,不然下一次使用的时候会引入上次使用的干扰。
池化需要注意的问题?
- 对象池也是一种缓存,缓存不宜过大,需要根据具体的情况来设定合适的池大小。
- 对象池为空的时候要有适当的处理,此时可以新建一个对象返回给客户,不能让客户无限等待。
- 多线程控制,这个时候可以选择线程同步,不过更适合的是 ThreadLocal,直接规避多线程问题。