Catch

V1

2022/04/13阅读:23主题:全栈蓝

设计模式之享元模式

享元模式是一个不太常用的设计模式,享元模式即共享的单元,其意图是复用对象,节省内存,前提是享元对象是不可变对象。

当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

类图

类图
类图

代码实现

这里我们以围棋棋子为例,棋子有黑子和白子,我们将棋子颜色与棋子位置分离,尽可能复用对象

Location

/**
 * @author Catch
 * @since 2022-04-12
 */

public class Location {

    private Integer x;
    private Integer y;

    public Location(Integer x, Integer y) {
        this.x = x;
        this.y = y;
    }

    public Integer getX() {
        return x;
    }

    public Integer getY() {
        return y;
    }

}

Piece

/**
 * @author Catch
 * @since 2022-04-12
 */

public class Piece {

    // 内部状态
    private String color;

    public Piece(String color) {
        this.color = color;
    }

    // 外部状态
    public void display(Location location) {
        System.out.println(color + "棋子: x=" + location.getX() + ", y=" + location.getY());
    }

    public String getColor() {
        return color;
    }

}

PieceFactory

public class PieceFactory {

    private static final Map<String, Piece> MAP = new HashMap<>();

    public static Piece getPiece(String color) {
        Piece piece = MAP.get(color);
        if (piece != null) {
            return piece;
        }
        piece = new Piece(color);
        MAP.put(color, piece);
        return piece;
    }

}

Main

public class Main {

    public static void main(String[] args) {
        Piece blackPiece = PieceFactory.getPiece("黑色");
        Piece whitePiece = PieceFactory.getPiece("白色");

        blackPiece.display(new Location(11));
        whitePiece.display(new Location(22));
    }

}

享元模式与单例、缓存、对象池的区别

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。享元模式有点类似于单例模式的变体多例模式。但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

使用缓存,主要是为了提高访问效率,而非复用节省内存。

池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

在 JDK 中的应用

Integer

查看下面的代码,思考一下,输出结果会是什么?

Integer i1 = 18;
Integer i2 = 18;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

上面的输出结果会是

true
false

为什么会是这样?要了解这个,我们首先要了解 Java 的自动装箱(Autoboxing)和自动拆箱(Unboxing)。

当我们把一个基本数据类型赋值给一个包装类型的时候,会触发自动装箱操作,如Integer i = 35;,底层实际执行了Integer i = Integer.valueOf(35);;反之,当我们把一个包装类型赋值给一个基本数据类型的时候,会触发自动拆箱,如int j = i;,底层实际执行了int j = i.intValue();

在 Java 中==比较的是两个对象的内存地址,因此我们首先看下装箱的过程究竟做了什么?下面是 Integer 相关的源码

public static Integer valueOf(int i) {
 if (i >= IntegerCache.low && i <= IntegerCache.high)
  return IntegerCache.cache[i + (-IntegerCache.low)];
 return new Integer(i);
}

/**
 * Cache to support the object identity semantics of autoboxing for values between
 * -128 and 127 (inclusive) as required by JLS.
 *
 * The cache is initialized on first usage.  The size of the cache
 * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
 * During VM initialization, java.lang.Integer.IntegerCache.high property
 * may be set and saved in the private system properties in the
 * sun.misc.VM class.
 */

private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

这里的IntegerCache就是享元模式的一种应用,它首先比较当前值是否在IntegerCache内,如果在则直接从IntegerCache的 cache 数组中取出,否则调用 new 新生产对象。IntegerCache 的static代码块在类加载的时候执行,可以看到在没有设置-XX:AutoBoxCacheMax=<size>时,其默认大小为[-128, 127]

如果你通过分析应用的 JVM 内存占用情况,发现 -128 到 255 之间的数据占用的内存比较多,你就可以用-XX:AutoBoxCacheMax=255-Djava.lang.Integer.IntegerCache.high=255的方式,将缓存的最大值从 127 调整到 255。不过,这里注意一下,JDK 并没有提供设置最小值的方法。

String

我们来看如下代码

String s1 = "a";
String s2 = "a";
String s3 = new String("a");
System.out.println(s1 == s2);
System.out.println(s1 == s3);

上面代码运行后返回如下

true
false

为什么会这样呢?JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。跟Integer的设计思路类似,但是Integer是在类加载的时候一次性创建好的,但是对于字符串,我们没法知道要共享哪些字符串常量,所以没办法提前创建好,能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可。

分类:

后端

标签:

后端

作者介绍

Catch
V1