reupe

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

面向对象编程的过程中,经常需要创建对象,如果频繁创建对象特别是使用容器持有对象,那么内存的占用就会越来越高,这对于大型项目来说有时候是致命的。比如对于一篇文档,文档中有文字,而文字是有字体信息、格式信息、颜色信息、位置信息等,显而易见,在面向对象编程中,每个文字被视作一个来处理,那么一篇文档中如果有几万个字,难道要创建几万个对象么?几十万个文字,就要创建几十万个对象么?这显然是极不合理的,实际上,文字处理器中经常用到我今天介绍的设计模式----享元模式, 在池化技术中享元模式得到了普遍应用。

 


1.享元模式

享元模式(Flyweight Pattern),运用共享技术有效地支持大量细粒度的对象。 ----《大话设计模式》

这里需要理解两个方面:

  •   怎么实现共享技术
  •       什么是细粒度对象

实现共享技术,就是什么时候创建对象、如何存储对象、怎样获取对象这一套策略。

细粒度对象,就像上文所举的文字例子,文字包含文字符号、位置等信息,这些信息中,注意到,文字符号是不可变的,就拿字母“A”来说,是不变的,但是位置是可变的,文章各个位置都有可能存在“A”这个字母,我们把位置信息抽取出来,称之为外部状态,剩下的部分,只有一个不变的符号信息,留在对象内部,称之为内部状态。这样把部分信息剥离只留下必要信息的对象,就叫做细粒度的对象。

 外部状态通常作为方法参数的形式传入“A”对象, 内部状态则作为“A”对象的成员变量。而“A”对象则可以被共享和复用,称之为享元对象。享元对象需要满足不可变性(immutability),即其内部状态不可被修改。

 总之,享元模式是为了减少内存开销而存在的,所以当编程过程中,如果遇到需要大量对象或者需要大量内存空间的时候,就可以考虑能不能使用享元模式。其类图为:

 

Flyweight就是上文提到的“细粒度对象”, 是需要被创建、存储和共享的, 它的实现类可以根据业务实际需求定义, UnsharedFlyweight1这里是不希望被共享的对象。FlyweightFactory是享元对象的工厂,客户端调用getFlyweight(key)获取Flyweight对象。注意到operation(外部状态)方法, 仅通过该方法将外部状态信息作为参数传入对象,比如字符“A”的位置信息。

 


2.代码实现

下面通过一个简易的字母处理器代码Demo来实现享元模式, 假设该处理器只支持英文26个小写字母。

定义了LowerCase类表示小写字母。使用WeakHashMap是为了最大程度上减少内存开销,因为其内持有的对象是弱引用,所以即使你没有特意处理,WeakhashMap中的对象也有可能被GC(垃圾回收)掉。WeakHashMap也是为缓存应用而生的, 这样有我们要的对象时给我们,没有时创建,我们用完之后,GC有可能会回收内存资源,所以在一定程度上可以优化内存损耗。

getCase()用来获取对象。

/**
 * 字母类,作为flyweight
 * 提供静态方法获取字母对象
 */
class LowerCase {
    /** 内部状态: 字母名称 */
    private final String name;
    /** 用缓存来储存共享对象 */
    private static final WeakHashMap<String, LowerCase> CACHE = new WeakHashMap<>();
    /** 只允许intern方法创建对象*/
    private LowerCase(String name) {
        this.name = name;
    }
    /**静态工厂方法: 获取共享对象*/
    public static LowerCase getCase(String key){
        //由于是共享的,这里保证多线程下不会重复创建对象
        synchronized (CACHE) {
            //如果存在key对应的对象,返回该对象;如果不存在,则创建一个对象
            return CACHE.computeIfAbsent(key, LowerCase::new);
        }
    }

    /** 用于测试CACHE中创建了多少对象*/
    public static int totalCount() {
        //共享对象可增删,使用同步代码块保证同时只有一个线程可以访问缓存
        synchronized (CACHE) {
            return CACHE.size();
        }
    }

    public String toString() {
        return name;
    }
}

 

文档类, 作为载体,字母被打印在上面,注意到printCase(String charactor, int positionX, int postionY)方法,charactor是缓存中所需LowerCase的key, positionX,positionY是表示该字符的坐标位置的外部信息,外部信息通畅就像这样通过方法传递的。

/**
 * 文档类,打印字母的版面
 */
class Document {
    public static void printCase(String charactor, int positionX, int positionY) {
        LowerCase lowerCase = LowerCase.getCase(charactor);
        Printer.print(lowerCase, positionX, positionY);
    }
}

 

 Printer模拟打印程序,拿到具体的字符对象LowerCase以及外部信息,对这些参数做整合处理。

/**
 * 打印程序,用来打印文字
 */
class Printer {
    public static void print(LowerCase lc, int x, int y) {
        System.out.printf("小写字母:%s, 位置(x=%d, y=%d)\n", lc.toString(),x,y);
    }
}

 

客户端调用,每一次随机从a,b,c,d,e,f这六个小写字母中取一个字母打印在随机的位置上,这个操作进行1000次,最后用LowerCase.totalCount()来查看缓存中有多少对象。

public class FlyWeightDemo1 {
    static final String[] keys = {"a", "b", "c", "d", "e", "f"};
    static Random rand = new Random(47);
    static void test(int circle) {
        for (int i = 0; i < circle; i++) {
            int index = rand.nextInt(keys.length);
            int x = rand.nextInt(1000);
            int y = rand.nextInt(1000);
            Document.printCase(keys[index], x, y);
        }
    }
    public static void main(String[] args) {
        //打印1000个字母
        test(1000);
        //查看缓存中目前一共有多少对象
        System.out.printf("缓存中共有%d个字母对象",LowerCase.totalCount());
    }
}

 

输出结果,输出了1000个字母,可以看到实际上缓存只保存了6个对象

小写字母:b, 位置(x=359, y=654)
小写字母:e, 位置(x=187, y=144)
小写字母:e, 位置(x=549, y=384)
......
......
......
小写字母:b, 位置(x=766, y=207)
小写字母:b, 位置(x=558, y=462)
缓存中共有6个字母对象

 

3.总结

优化内存损耗是开发应用过程中经常需要考虑的问题,只要遇见需要大量创建类似的对象或者需要大量内存的时候,就可以考虑这个对象能否把“外部状态”剥离出来从而由剩下的部分形成所谓“细粒度对象”。在本文的代码示例中,采用WeakHashMap,应用享元模式,实现了一个简化版的“字母池”, 每次需要创建一个字母,从缓存中去取,从而大大节省了内存空间。在实际开发中,大可不必非得按照“标准”UML类图,创建一个Flyweight,一个FlyweightFactory,享元模式的核心是创建、储存和获取对象,其目的是节省内存空间,只要按照这种思路对代码进行设计即可。

此外,由于享元模式的享元对象,是需要被共享的,当创建享元对象时,就需要考虑到多线程并发的情况。一般分为两种情况:

  1. 一种是储存享元对象的容器大小是有限的,这样可以提前创建好对象,多线程去获取对象就不存在抢占问题了。
  2. 另一种是运行时多线程可能会创建享元对象(如本文代码),那么就需要考虑线程安全问题,一般有两种方式:  
      • 采用sychronized同步线程,任何一个时刻只允许一个线程访问缓存资源(如本文代码)
      • 享元对象满足相等性(equality)的前提下,可以允许多线程同时创建对象和访问
posted on 2019-02-21 09:46  yxlaisj  阅读(508)  评论(0编辑  收藏  举报