一天一种JAVA设计模式之十二:享元模式

431 阅读11分钟

写在前面的话

复习、总结23种设计模式

获取详细源码请点击我

上一篇

# 一天一种JAVA设计模式之十一:组合模式

享元模式

记重点

本质:缓存共享对象,降低内存消耗

享元模式和工厂模式的区别

前者着重于getOrCreate,对象的复用,节省内存

后者着重于create,生产对象(细节对客户端屏蔽)

使用场景:Integer缓存,Long缓存、连接池等

定义

享元模式(Flyweight Pattern)是池技术的重要实现方式,其定义如下:Use sharing tosupport large numbers of fine-grained objects efficiently.(使用共享对象可有效地支持大量的细粒度的对象。) 享元模式结构较为复杂,一般结合工厂模式一起使用。

享元模式的定义为我们提出了两个要求:细粒度的对象和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免呢?就是享元模式提到的共享技术。我们先来了解一下对象的内部状态和外部状态。要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)

内部状态

内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变,。它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。

外部状态

外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如我们例子中的考试科目+考试地点复合字符串,它是一批对象的统一标识,是唯一的一个索引值。有了对象的两个状态,我们就可以来看享元模式的通用类图。

例如:连接池中的连接对象,保存在连接对象中的用户名、密码、连接等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而每个连接要回收利用时,需要给它标记为可用状态,这些为外部状态

享元模式的通用类图

image.png

享元模式的角色

Flyweight--抽象享元角色

它简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。

抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法。在抽象角色中,一般需要把外部状态和内部状态(当然了,可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。

package com.design.pattern.flyweight.test01;  
  
// 抽象享元角色  
public abstract class Flyweight {  
  
    // 内部状态  
    private String intrinsic;  
    // 外部状态  
    protected final String Extrinsic;  

    // 要求享元角色必须接受外部状态  
    public Flyweight(String _Extrinsic) {  
        this.Extrinsic = _Extrinsic;  
    }  

    public abstract void operate();  

    public String getIntrinsic() {  
        return intrinsic;  
    }  

    public void setIntrinsic(String intrinsic) {  
        this.intrinsic = intrinsic;  
    }  
  
}

享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中

ConcreteFlyweight--具体享元角色

具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。

package com.design.pattern.flyweight.test01;  
  
// 具体享元角色1  
public class ConcreteFlyweight1 extends Flyweight{  
  
    // 接受外部状态  
    public ConcreteFlyweight1(String _Extrinsic) {  
        super(_Extrinsic);  
    }  

    // 根据外部状态进行逻辑处理  
    @Override  
    public void operate() {  
        // 业务逻辑  
        System.out.println(Extrinsic);  
    }  
  
}

unsharedConcreteFlyweight---不可共享的享元角色

不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。

FlyweightFactory--享元工厂

职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。

package com.design.pattern.flyweight.test01;  
  
import java.util.HashMap;  
  
// 享元工厂  
// 此木为柴山山出,因火成烟夕夕多  
public class FlyweightFactory {  
  
    private static HashMap<String, Flyweight> pool = new HashMap<>();  

    public static Flyweight getFlyweight(String Extrinsic) {  
        Flyweight flyweight = null;  
        if (pool.containsKey(Extrinsic)) {  
            flyweight = pool.get(Extrinsic);  
        } else {  
            // 根据外部状态创建享元对象  
            flyweight = new ConcreteFlyweight1(Extrinsic);  
            pool.put(Extrinsic, flyweight);  
        }  
        return flyweight;  
    }  
  
}

享元模式的目的在于运用共享技术,使得一些细粒度的对象可以共享,我们的设计确实也应该这样,多使用细粒度的对象,便于重用或重构。

享元模式的优点和缺点

享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但它同时也提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。

1,优点

极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
享元模式中的外部状态相对独立,且不影响内部状态

2,缺点:

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长

享元模式的使用场景

在如下场景中则可以选择使用享元模式。

  1. 系统中存在大量的相似对象,造成内存的大量耗费
  2. 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
  3. 需要缓冲池的场景
  4. 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

源码分析享元模式的典型应用

String中的享元模式

Java中将String类定义为final(不可改变的),JVM中字符串一般保存在字符串常量池中,java会确保一个字符串在常量池中只有一个拷贝,这个字符串常量池在JDK6.0以前是位于常量池中,位于永久代,而在JDK7.0中,JVM将其从永久代拿出来放置于堆中。

我们做一个测试:

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        String s3 = "he" + "llo";
        String s4 = "hel" + new String("lo");
        String s5 = new String("hello");
        String s6 = s5.intern();
        String s7 = "h";
        String s8 = "ello";
        String s9 = s7 + s8;
        System.out.println(s1==s2);//true
        System.out.println(s1==s3);//true
        System.out.println(s1==s4);//false
        System.out.println(s1==s9);//false
        System.out.println(s4==s5);//false
        System.out.println(s1==s6);//true
    }
}

String类的final修饰的,以字面量的形式创建String变量时,jvm会在编译期间就把该字面量hello放到字符串常量池中,由Java程序启动的时候就已经加载到内存中了。这个字符串常量池的特点就是有且只有一份相同的字面量,如果有其它相同的字面量,jvm则返回这个字面量的引用,如果没有相同的字面量,则在字符串常量池创建这个字面量并返回它的引用。

由于s2指向的字面量hello在常量池中已经存在了(s1先于s2),于是jvm就返回这个字面量绑定的引用,所以s1==s2

s3中字面量的拼接其实就是hello,jvm在编译期间就已经对它进行优化,所以s1和s3也是相等的。

s4中的new String("lo")生成了两个对象,lonew String("lo")lo存在字符串常量池,new String("lo")存在堆中,String s4 = "hel" + new String("lo")实质上是两个对象的相加,编译器不会进行优化,相加的结果存在堆中,而s1存在字符串常量池中,当然不相等。s1==s9的原理一样。

s4==s5两个相加的结果都在堆中,不用说,肯定不相等。

s1==s6中,s5.intern()方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池并返回它的引用。因此s1==s6输出true。

Integer 中的享元模式

使用例子如下:

    public static void main(String[] args) {
        Integer i1 = 12 ;
        Integer i2 = 12 ;
        System.out.println(i1 == i2); // true

        Integer b1 = 128 ;
        Integer b2 = 128 ;
        System.out.println(b1 == b2); // false
    }

为什么第一个是true,第二个是false? 反编译后可以发现 Integer b1 = 128; 实际变成了 Integer b1 = Integer.valueOf(128);,所以我们来看 Integer 中的 valueOf 方法的实现

public final class Integer extends Number implements Comparable<Integer> {
    public static Integer valueOf(int var0) {
        return var0 >= -128 && var0 <= Integer.IntegerCache.high ? Integer.IntegerCache.cache[var0 + 128] : new Integer(var0);
    }
    //...省略...
}

IntegerCache 缓存类

    //是Integer内部的私有静态类,里面的cache[]就是jdk事先缓存的Integer。
    private static class IntegerCache {
        static final int low = -128;//区间的最低值
        static final int high;//区间的最高值,后面默认赋值为127,也可以用户手动设置虚拟机参数
        static final Integer cache[]; //缓存数组

        static {
            // high value may be configured by property
            int h = 127;
            //这里可以在运行时设置虚拟机参数来确定h  :-Djava.lang.Integer.IntegerCache.high=250
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {//用户设置了
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);//虽然设置了但是还是不能小于127
                // 也不能超过最大值
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            //循环将区间的数赋值给cache[]数组
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
    }

可以看到 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象

Long中的享元模式

public final class Long extends Number implements Comparable<Long> {
    public static Long valueOf(long var0) {
        return var0 >= -128L && var0 <= 127L ? Long.LongCache.cache[(int)var0 + 128] : new Long(var0);
    }   
    private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }
    //...
}

同理,Long 中也有缓存,不过不能指定缓存最大值

Apache Commons Pool2中的享元模式

对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称为“对象池”(Object Pool,或简称Pool)

Apache Commons Pool实现了对象池的功能。定义了对象的生成、销毁、激活、钝化等操作及其状态转换,并提供几个默认的对象池实现。

有几个重要的对象:

PooledObject(池对象) :用于封装对象(如:线程、数据库连接、TCP连接),将其包裹成可被池管理的对象。
PooledObjectFactory(池对象工厂) :定义了操作PooledObject实例生命周期的一些方法,PooledObjectFactory必须实现线程安全。
Object Pool (对象池) :Object Pool负责管理PooledObject,如:借出对象,返回对象,校验对象,有多少激活对象,有多少空闲对象。

 // 对象池
 private final Map<S, PooledObject<S>> allObjects = new ConcurrentHashMap<S, PooledObject<S>>();

重要方法:

borrowObject:从池中借出一个对象。
returnObject:将一个对象返还给池。

下一篇

# 一天一种JAVA设计模式之十一:组合模式