程序员人生 网站导航

java/android 设计模式学习笔记(5)---对象池模式

栏目:综合技术时间:2016-06-15 14:12:10

  这次要介绍1下对象池模式(Object Pool Pattern),这个模式为常见 23 种设计模式以外的设计模式,介绍的初衷主要是在平时的 android 开发中常常会看到,比如 ThreadPool 和 MessagePool 等。
  在 java 中,所有对象的内存由虚拟机管理,所以在某些情况下,需要频繁创建1些生命周期很短使用完以后就能够立即烧毁,但是数量很大的对象集合,那末此时 GC 的次数必定会增加,这时候候为了减小系统 GC 的压力,对象池模式就很适用了。对象池模式也是创建型模式之1,它不是根据使用动态的分配和烧毁内存,而是保护1个已初始化好若干对象的对象池以供使用。客户端使用的时候从对象池中去申请1个对象,当该对象使用完以后,客户端会返回给对象池,而不是立即烧毁它,这步操作可以手动或自动完成。
  从 Java 语言的特性来分析1下,在 Java 中,对象的生命周期大致包括3个阶段:对象的创建,对象的使用,对象的清除。因此,对象的生命周期长度可用以下的表达式表示:T = T1 + T2 +T3。其中T1表示对象的创建时间,T2 表示对象的使用时间,而 T3 则表示其清除时间。由此,我们可以看出,只有 T2 是真正有效的时间,而 T1、T3 则是对象本身的开消。下面再看看 T1、T3 在对象的全部生命周期中所占的比例。Java对象是通过构造函数来创建的,在这1进程中,该构造函数链中的所有构造函数也都会被自动调用。另外,默许情况下,调用类的构造函数时,Java 会把变量初始化成肯定的值:所有的对象被设置成 null,整数变量(byte、short、int、long)设置成 0, float 和 double 变量设置成 0.0,逻辑值设置成false。所以用new关键字来新建1个对象的时间开消是很大的,以下表所示:

运算操作 示例 标准化时间
本地赋值 i = n 1.0
实例赋值 this.i = n 1.2
方法调用 Funct() 5.9
新建对象 New Object() 980
新建数组 New int[10] 3100

从表中可以看出,新建1个对象需要980个单位的时间,是本地赋值时间的980倍,是方法调用时间的166倍,而若新建1个数组所花费的时间就更多了。
  再看清除对象的进程,我们知道,Java 语言的1个优势,就是 Java 程序员勿需再像 C/C++ 程序员那样,显式地释放对象,而由称为垃圾搜集器(Garbage Collector)的自动内存管理系统,定时或在内存凸现出不足时,自动回收垃圾对象所占的内存。凡事有益总也有弊,这虽然为 Java 程序设计者提供了极大的方便,但同时它也带来了较大的性能开消。这类开消包括两方面,首先是对象管理开消,GC为了能够正确释放对象,它必须监控每个对象的运行状态,包括对象的申请、援用、被援用、赋值等。其次,在 GC 开始回收“垃圾”对象时,系统会暂停利用程序的履行,而独自占用 CPU。
  因此,如果要改良利用程序的性能,1方面应尽可能减少创建新对象的次数;同时,还应尽可能减少 T1、T3 的时间,而这些都可以通过对象池技术来实现。所以对象池主要是用来提升性能,在某些情况下,对象池对性能有极大的帮助。但是还有1点需要注意,对象池会增加对象生命周期的复杂度,这是由于从对象池获得的对象和返还给对象池的对象都没有真实的创建或烧毁。
  PS:对技术感兴趣的同鞋加群5446459721起交换。

设计模式总目录

  java/android 设计模式学习笔记目录

特点

  从上面的介绍可以总结:对象池模式适用于“需要使用到大量的同1类对象,这些对象的初始化会消耗大量的系统资源,而且它们只需要使用很短的时间,这类操作会对系统的性能有1定影响”的情况,总结1下就是在下面两种分配模式下可以选择使用对象池:

  • 对象以固定的速度不断地分配,垃圾搜集时间逐渐增加,内存使用率随之增大;
  • 对象分配存在爆发期,而每次爆发都会致使系统迟滞,并伴随明显的GC中断。
在绝大多数情况下,这些对象要末是数据容器,要末是数据封装器,其作用是在利用程序和内部消息总线、通讯层或某些API之间充当1个信封。这很常见。例如,数据库驱动会针对每一个要求和响应创建 Request 和 Response 对象,消息系统会使用 Message 和 Event 封装器,等等。对象池可以帮助保存和重用这些构造好的对象实例。
  对象池模式会预先创建并初始化好1个对象集适用于重用,当某处需要1个该类型的新对象时,它直接向对象池申请,如果此时对象池中有已预先初始化好的对象,它就直接返回,如果没有,对象池就会创建1个并且返回;当使用完该对象以后,它会将该对象返还给对象池,对象池会将其重用以免该对象笨重的初始化操作。有1点需要注意的是,1旦1个对象被对象池重用返回给需要使用的地方以后,该对象已存在援用都将会变成非法不可以使用。需要注意的是,在1些系统资源很紧缺的系统上,对象池模式将会限制对象池最大的大小,当已到达最大的数量以后,再次获得可能会抛出异常或直接被阻塞直到有1个对象被对象池回收。

UML类图

这里写图片描述
  1般对象池模式的 uml 类图如图所示,他有3个角色

  • Reusable:对象类,该对象在实际使用中需要频繁的创建和烧毁,并且初始化操作会很很消耗时间和性能;
  • Client:使用 Reusable 类对象的角色;
  • ReusablePool:管理 Reusable 对象类的所有对象,供 Client 角色使用。
  通常情况下,我们希望所有的 Reusable 对象都会被 ResuablePool 管理,所以可使用单例模式构建 ReusablePool,固然其他的工厂方法模式也是可以的 。Client 调用 getInstance 方法获得到 ReusablePool 的对象,接着调用 acquireReusable 方法去获得 Reusable 对象,使用完以后调用 releaseReusable 方法传入该对象重新交给 ReusablePool 管理。

示例与源码

  我们先以 android 源码中的 MessagePool 为例子来分析1下 ObjectPool 在实际开发中的使用效果,以后写1个小的 demo 。

MessagePool源码分析

  在 android 中使用 new Message() , Message.obtain() 和 Handler.obtainMessage() 都能够取得1个 Message 对象,但是为何后两个的效力会高于前者呢?先来看看源码的 Message.obtain() 函数:

/** * Return a new Message instance from the global pool. Allows us to * avoid allocating new objects in many cases. */ public static Message obtain() { synchronized (sPoolSync) { if (sPool != null) { Message m = sPool; sPool = m.next; m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }

返回的是 sPool 这个对象,继续看看 Handler.obtainMessage() 函数:

/** * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this). * If you don't want that facility, just call Message.obtain() instead. */ public final Message obtainMessage() { return Message.obtain(this); }

1样是调用到了 Message.obtain() 函数,那末我们就从这个函数开始分析, Message 类是如何构建 MessagePool 这个角色的呢?看看这几处的代码:

//变量的构建 private static final Object sPoolSync = new Object(); private static Message sPool; private static int sPoolSize = 0; private static final int MAX_POOL_SIZE = 50; private static boolean gCheckRecycle = true;

Message 对象的回收

/** * Return a Message instance to the global pool. * <p> * You MUST NOT touch the Message after calling this function because it has * effectively been freed. It is an error to recycle a message that is currently * enqueued or that is in the process of being delivered to a Handler. * </p> */ public void recycle() { if (isInUse()) { if (gCheckRecycle) { throw new IllegalStateException("This message cannot be recycled because it " + "is still in use."); } return; } recycleUnchecked(); } /** * Recycles a Message that may be in-use. * Used internally by the MessageQueue and Looper when disposing of queued Messages. */ void recycleUnchecked() { // Mark the message as in use while it remains in the recycled object pool. // Clear out all other details. flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = -1; when = 0; target = null; callback = null; data = null; synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { next = sPool; sPool = this; sPoolSize++; } } }

从这几处代码可以很清楚的看到:

  • sPool 这个静态对象实际上是相当于构建了1个长度为 MAX_POOL_SIZE 的链表, recycleUnchecked 方法之内置锁的方式(线程安全),判断当前对象池的大小是不是小于50,若小于50,则会被加到链表的头部,并把 next 对象指向上1次链表的头部;若大于等于50,则直接抛弃掉,那末这些被抛弃的Message将交由GC处理。
  • recycleUnchecked 方法将待回收的Message对象字段先置空,以便节省 MessagePool 的内存和方便下1次的直接使用;
源码这么1看, android 中 MessagePool 其实实现方式还是很简单的。

示例

  我们参照上面的 uml 类图写1个 demo,首先要定义 Reusable 这个角色,为了直观,我们将该类定义了很多成员变量,用来摹拟 expensive initialization:
Reusable.class

public class Reusable { public String a; public String b; public String c; public String d; public String e; public String f; public String g; public ArrayList<String> h; public ArrayList<String> i; public ArrayList<String> j; public ArrayList<String> k; public ArrayList<String> l; public Reusable(){ h = new ArrayList<>(); i = new ArrayList<>(); j = new ArrayList<>(); k = new ArrayList<>(); l = new ArrayList<>(); } }

然后用1个 IReusablePool.class接口来定义对象池的基本行动:

public interface IReusablePool { Reusable requireReusable(); void releaseReusable(Reusable reusable); void setMaxPoolSize(int size); }

最后实现该接口:

public class ReusablePool implements IReusablePool { private static final String TAG = "ReusablePool"; private static volatile ReusablePool instance = null; private static List<Reusable> available = new ArrayList<>(); private static List<Reusable> inUse = new ArrayList<>(); private static final byte[] lock = new byte[]{}; private static int maxSize = 5; private int currentSize = 0; private ReusablePool() { available = new ArrayList<>(); inUse = new ArrayList<>(); } public static ReusablePool getInstance() { if (instance == null) { synchronized (ReusablePool.class) { if (instance == null) { instance = new ReusablePool(); } } } return instance; } @Override public Reusable requireReusable() { synchronized (lock) { if (currentSize >= maxSize) { throw new RuntimeException("pool has gotten its maximum size"); } if (available.size() > 0) { Reusable reusable = available.get(0); available.remove(0); currentSize++; inUse.add(reusable); return reusable; } else { Reusable reusable = new Reusable(); inUse.add(reusable); currentSize++; return reusable; } } } @Override public void releaseReusable(Reusable reusable) { if (reusable != null) { reusable.a = null; reusable.b = null; reusable.c = null; reusable.d = null; reusable.e = null; reusable.f = null; reusable.g = null; reusable.h.clear(); reusable.i.clear(); reusable.j.clear(); reusable.k.clear(); reusable.l.clear(); } synchronized (lock) { inUse.remove(reusable); available.add(reusable); currentSize--; } } @Override public void setMaxPoolSize(int size) { synchronized (lock) { maxSize = size; } } }

最后测试程序:

@Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_get: new Thread(new Runnable() { @Override public void run() { try { Reusable reusable = ReusablePool.getInstance().requireReusable(); Log.e("CORRECT", "get a Reusable object " + reusable); Thread.sleep(5000); ReusablePool.getInstance().releaseReusable(reusable); }catch (Exception e){ Log.e("ERROR", e.getMessage()); } } }).start(); break; } }

需要说明的是:

  • releaseReusable 方法不要忘记将 Reusable 对象中的成员变量重置,要不然下次使用会有问题;
  • 摹拟了 5s 的延时,在 demo 中如果到达了 maxPoolSize ,继续操作是抛出 RunTimeException ,这个在实际使用该进程中可以依照情况更改(可以抛出异常,新建1个对象返回或直接在多线程模式下阻塞,直到有新对象,参考链接:
    https://en.wikipedia.org/wiki/Object_pool_pattern#Handling_of_empty_pools);
  • 在程序中使用的是单例模式获得的 ReusablePool 对象,这个在实际使用进程中也能够换成工厂方法模式等的其他设计模式。

总结

  对象池模式从上面来分析可以说是用途很大,但是这个模式1定要注意使用的场景,也就是最上面提到的两点:“对象以固定的速度不断地分配,垃圾搜集时间逐渐增加,内存使用率随之增大”和“对象分配存在爆发期,而每次爆发都会致使系统迟滞,并伴随明显的GC中断”,其他的1般情况下最好不要使用对象池模式,我们后面还会提到它的缺点和很多人对其的批评。

优点

  优点上面就已论述的很清楚了,对那些”the rate of instantiation and destruction of a class is high” 和 “the cost of initializing a class instance is high”的场景优化是及其明显的,例如 database connections,socket connections,threads 和类似于 fonts 和 Bitmap 之类的对象。

圈套

  上面已提过了,使用对象池模式时,每次进行回收操作前都需要将该对象的相干成员变量重置,如果不重置将会致使下1次重复使用该对象时候出现预感不到的毛病,同时的,如果回收对象的成员变量很大,不重置还可能会出现内存 OOM 和信息泄漏等问题。另外的,对象池模式绝大多数情况下都是在多线程中访问的,所以做好同步工作也是极为重要的。原文:https://en.wikipedia.org/wiki/Object_pool_pattern#Pitfalls
  除以上两点以外,还需要注意以下问题:

  • 援用泄漏:对象在系统中某个地方注册了,但没有返回到池中。
  • 过早回收:消费者已决定将对象返还给对象池,但依然持有它的援用,并试图履行写或读操作,这时候会出现这类情况。
  • 隐式回收:当使用援用计数时可能会出现这类情况。
  • 大小毛病:这类情况在使用字节缓冲区和数组时非常常见:对象应当有不同的大小,而且是以定制的方式构造,但返回对象池后却作为通用对象重用。
  • 重复下单:这是援用泄漏的1个变种,存在多路复用时特别容易产生:1个对象被分配到多个地方,但其中1个地方释放了该对象。
  • 就地修改:对象不可变是最好的,但如果不具有那样做的条件,便可能在读取对象内容时遇到内容被修改的问题。
  • 缩小对象池:当池中有大量的未使用对象时,要缩小对象池。
对如何根据使用情况放大和缩小对象池的大小,1个广为人知但鲜有人用的技能:对象池 这篇文章中讲述的很清楚,感兴趣的可以看看。

讨论与批评

  1些开发者不建议在某些语言例如 Java 中使用对象池模式,特别是对象只使用内存并且不会持有其他资源的语言。这些反对者持有的观点是 new 的操作只需要 10 条指令,而使用对象池模式则需要成百上千条指令,明显增加了复杂度,而且 GC 操作只会去扫描“活着”的对象援用所指向的内存,而不是它们的成员变量所使用的那块内存,这就意味着,任何没有援用的“死亡”对象都能够被 GC 以很小的代价跳过,相反如果使用对象池模式,持有大量“活着“的对象反而会增加 GC 的时间。原文:https://en.wikipedia.org/wiki/Object_pool_pattern#Criticism

源码下载

  https://github.com/zhaozepeng/Design-Patterns/tree/master/ObjectPoolPattern

援用

http://www.cnblogs.com/xinye/p/3907642.html
https://en.wikipedia.org/wiki/Object_pool_pattern
http://blog.csdn.net/liyangbing315/article/details/4942870
http://www.infoq.com/cn/news/2015/07/ClojureWerkz
http://blog.csdn.net/xplee0576/article/details/46875555

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐