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 设计模式学习笔记目录

特点

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

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

时间: 2024-07-30 10:09:26

java/android 设计模式学习笔记(5)---对象池模式的相关文章

java/android 设计模式学习笔记(一)---单例模式

前段时间公司一些同事在讨论单例模式(我是最渣的一个,都插不上嘴 T__T ),这个模式使用的频率很高,也可能是很多人最熟悉的设计模式,当然单例模式也算是最简单的设计模式之一吧,简单归简单,但是在实际使用的时候也会有一些坑. PS:对技术感兴趣的同鞋加群544645972一起交流 设计模式总目录 java/android 设计模式学习笔记目录 特点 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 单例模式的使用很广泛,比如:线程池(threadpool).缓存(cache).对

java/android 设计模式学习笔记(13)---享元模式

这篇我们来介绍一下享元模式(Flyweight Pattern),Flyweight 代表轻量级的意思,享元模式是对象池的一种实现.享元模式用来尽可能减少内存使用量,它适合用于可能存在大量重复对象的场景,缓存可共享的对象,来达到对象共享和避免创建过多对象的效果,这样一来就可以提升性能,避免内存移除和频繁 GC 等. 享元模式的一个经典使用案例是文本系统中图形显示所用的数据结构,一个文本系统能够显示的字符种类就是那么几十上百个,那么就定义这么些基础字符对象,存储每个字符的显示外形和其他的格式化数据

java/android 设计模式学习笔记(16)---命令模式

这篇博客我们来介绍一下命令模式(Command Pattern),它是行为型设计模式之一.命令模式相对于其他的设计模式更为灵活多变,我们接触比较多的命令模式个例无非就是程序菜单命令,如在操作系统中,我们点击关机命令,系统就会执行一系列的操作,如先是暂停处理事件,保存系统的一些配置,然后结束程序进程,最后调用内核命令关闭计算机等,对于这一系列的命令,用户不用去管,用户只需点击系统的关机按钮即可完成如上一系列的命令.而我们的命令模式其实也与之相同,将一系列的方法调用封装,用户只需调用一个方法执行,那

java/android 设计模式学习笔记(14)---外观模式

这篇博客来介绍外观模式(Facade Pattern),外观模式也称为门面模式,它在开发过程中运用频率非常高,尤其是第三方 SDK 基本很大概率都会使用外观模式.通过一个外观类使得整个子系统只有一个统一的高层的接口,这样能够降低用户的使用成本,也对用户屏蔽了很多实现细节.当然,在我们的开发过程中,外观模式也是我们封装 API 的常用手段,例如网络模块.ImageLoader 模块等.其实我们在开发过程中可能已经使用过很多次外观模式,只是没有从理论层面去了解它. 转载请注明出处:http://bl

java/android 设计模式学习笔记(10)---建造者模式

这篇博客我们来介绍一下建造者模式(Builder Pattern),建造者模式又被称为生成器模式,是创造性模式之一,与工厂方法模式和抽象工厂模式不同,后两者的目的是为了实现多态性,而 Builder 模式的目的则是为了将对象的构建与展示分离.Builder 模式是一步一步创建一个复杂对象的创建型模式,它允许用户在不知道内部构建细节的情况下,可以更精细地控制对象的构造流程.一个复杂的对象有大量的组成部分,比如汽车它有车轮.方向盘.发动机.以及各种各样的小零件,要将这些部件装配成一辆汽车,这个装配过

java/android 设计模式学习笔记(7)---装饰者模式

这篇将会介绍装饰者模式(Decorator Pattern),装饰者模式也称为包装模式(Wrapper Pattern),结构型模式之一,其使用一种对客户端透明的方式来动态的扩展对象的功能,同时它也是继承关系的一种替代方案之一,但比继承更加灵活.在现实生活中也可以看到很多装饰者模式的例子,或者可以大胆的说装饰者模式无处不在,就拿一件东西来说,可以给它披上无数层不一样的外壳,但是这件东西还是这件东西,外壳不过是用来扩展这个东西的功能而已,这就是装饰者模式,装饰者的这个角色也许各不相同但是被装饰的对

java/android 设计模式学习笔记(12)---组合模式

这篇我们来介绍一下组合模式(Composite Pattern),它也称为部分整体模式(Part-Whole Pattern),结构型模式之一.组合模式比较简单,它将一组相似的对象看作一个对象处理,并根据一个树状结构来组合对象,然后提供一个统一的方法去访问相应的对象,以此忽略掉对象与对象集合之间的差别.这个最典型的例子就是数据结构中的树了,如果一个节点有子节点,那么它就是枝干节点,如果没有子节点,那么它就是叶子节点,那么怎么把枝干节点和叶子节点统一当作一种对象处理呢?这就需要用到组合模式了. 转

java/android 设计模式学习笔记(9)---代理模式

这篇博客我们来介绍一下代理模式(Proxy Pattern),代理模式也成为委托模式,是一个非常重要的设计模式,不少设计模式也都会有代理模式的影子.代理在我们日常生活中也很常见,比如上网时连接的代理服务器地址,更比如我们平时租房子,将找房子的过程代理给中介等等,都是代理模式在日常生活中的使用例子. 代理模式中的代理对象能够连接任何事物:一个网络连接,一个占用很多内存的大对象,一个文件,或者是一些复制起来代价很高甚至根本不可能复制的一些资源.总之,代理是一个由客户端调用去访问幕后真正服务的包装对象

java/android 设计模式学习笔记(6)---适配器模式

这篇来介绍一下适配器模式(Adapter Pattern),适配器模式在开发中使用的频率也是很高的,像 ListView 和 RecyclerView 的 Adapter 等都是使用的适配器模式.在我们的实际生活中也有很多类似于适配器的例子,比如香港的插座和大陆的插座就是两种格式的,为了能够成功适配,一般会在中间加上一个电源适配器,形如: 这样就能够将原来不符合的现有系统和目标系统通过适配器成功连接. 说到底,适配器模式是将原来不兼容的两个类融合在一起,它有点类似于粘合剂,将不同的东西通过一种转