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

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

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

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

设计模式总目录

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

特点

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

  • 对象以固定的速度不断地分配,垃圾收集时间逐步增加,内存使用率随之增大;
  • 对象分配存在爆发期,而每次爆发都会导致系统迟滞,并伴有明显的GC中断。

在绝大多数情况下,这些对象要么是数据容器,要么是数据封装器,其作用是在应用程序和内部消息总线、通信层或某些API之间充当一个信封。这很常见。例如,数据库驱动会针对每个请求和响应创建 Request 和 Response 对象,消息系统会使用 Message 和 Event 封装器,等等。对象池可以帮助保存和重用这些构造好的对象实例。
对象池模式会预先创建并初始化好一个对象集合用于重用,当某处需要一个该类型的新对象时,它直接向对象池申请,如果此时对象池中有已经预先初始化好的对象,它就直接返回,如果没有,对象池就会创建一个并且返回;当使用完该对象之后,它会将该对象返还给对象池,对象池会将其重用以避免该对象笨重的初始化操作。有一点需要注意的是,一旦一个对象被对象池重用返回给需要使用的地方之后,该对象已经存在引用都将会变成非法不可使用。需要注意的是,在一些系统资源很紧缺的系统上,对象池模式将会限制对象池最大的大小,当已经达到最大的数量之后,再次获取可能会抛出异常或者直接被阻塞直到有一个对象被对象池回收。

享元模式对象池模式对比

看了上面的特征会让我们想到享元模式,确实,对象池模式享元模式有很多的相同点,但是他们有一个很重要的不同点:享元模式通常情况下获取的是不可变的实例,而从对象池模式中获取的对象通常情况下是可变的。所以使用享元模式用来避免创建多个拥有同样状态的对象,只创建一个并且在应用的不同地方都使用这一个实例;而对象池中的资源从使用的角度上看具有不同的状态并且每个都需要单独控制,但是又不想花费一定的资源区频繁的创建和销毁这些资源对象,毕竟他们都有相同的初始化过程。
简而言之,享元模式更加倾向于状态的不可变性,而对象池模式则是状态的可变性。

UML类图

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

  • Reusable:对象类,该对象在实际使用中需要频繁的创建和销毁,并且初始化操作会很很消耗时间和性能;
  • Client:使用 Reusable 类对象的角色;
  • ReusablePool:管理 Reusable 对象类的所有对象,供 Client 角色使用。

通常情况下,我们希望所有的 Reusable 对象都会被 ResuablePool 管理,所以可以使用单例模式构建 ReusablePool,当然其他的工厂方法模式也是可以的 。Client 调用 getInstance 方法获取到 ReusablePool 的对象,接着调用 acquireReusable 方法去获取 Reusable 对象,使用完之后调用 releaseReusable 方法传入该对象重新交给 ReusablePool 管理。

示例与源码

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

MessagePool源码分析

在 android 中使用 new Message() , Message.obtain() 和 Handler.obtainMessage() 都能够获得一个 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);
}

一样是调用到了 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 这个静态对象实际上是相当于构建了一个长度为 MAX_POOL_SIZE 的链表, recycleUnchecked 方法以内置锁的方式(线程安全),判断当前对象池的大小是否小于50,若小于50,则会被加到链表的头部,并把 next 对象指向上一次链表的头部;若大于等于50,则直接丢弃掉,那么这些被丢弃的Message将交由GC处理。
  • recycleUnchecked 方法将待回收的Message对象字段先置空,以便节省 MessagePool 的内存和方便下一次的直接使用;

源码这么一看, android 中 MessagePool 其实实现方式还是很简单的。

示例

我们参照上面的 uml 类图写一个 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<>();
    }
}

然后用一个 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 ,这个在实际使用该过程中可以按照情况更改(可以抛出异常,新建一个对象返回或者直接在多线程模式下阻塞,直到有新对象,参考链接:
    https://en.wikipedia.org/wiki/Object_pool_pattern#Handling_of_empty_pools);
  • 在程序中使用的是单例模式获取的 ReusablePool 对象,这个在实际使用过程中也可以换成工厂方法模式等的其他设计模式。

总结

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

优点

优点上面就已经阐述的很清楚了,对于那些”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 之类的对象。

陷阱

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

  • 引用泄露:对象在系统中某个地方注册了,但没有返回到池中。
  • 过早回收:消费者已经决定将对象返还给对象池,但仍然持有它的引用,并试图执行写或读操作,这时会出现这种情况。
  • 隐式回收:当使用引用计数时可能会出现这种情况。
  • 大小错误:这种情况在使用字节缓冲区和数组时非常常见:对象应该有不同的大小,而且是以定制的方式构造,但返回对象池后却作为通用对象重用。
  • 重复下单:这是引用泄露的一个变种,存在多路复用时特别容易发生:一个对象被分配到多个地方,但其中一个地方释放了该对象。
  • 就地修改:对象不可变是最好的,但如果不具备那样做的条件,就可能在读取对象内容时遇到内容被修改的问题。
  • 缩小对象池:当池中有大量的未使用对象时,要缩小对象池。

对于如何根据使用情况放大和缩小对象池的大小,一个广为人知但鲜有人用的技巧:对象池 这篇文章中讲述的很清楚,感兴趣的可以看看。

讨论与批判

一些开发者不建议在某些语言例如 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

本系列:



可能感兴趣的文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部