设计模式-享元模式(FlyWeight Pattern)

享元模式(FlyWeight Pattern)

  概要

  记忆关键字:细粒度、共享

  定义:运用共享技术有效地支持大量细粒度的对象

  分析:共享对象,将对象的一部分状态(内部状态)设计成可共享的,以减少对象的数量,达到节省内存的目的。

  翻译由来:FlyWeight 这个英文词汇直译更接近 "轻量级" 的含义,翻译为 "享元模式" 的原因可能是为了强调这种模式的核心思想,即共享和复用。设计模式的中文翻译有时候会采用更容易理解和贴近中文思维的术语,而 "享元模式" 在这方面表达得相对贴切。

  类型:结构型

  享元模式结构图如下:

   

   一、 涉及的角色
   1.  抽象享元类 Flyweight

   通常是接口或抽象类,它声明了具体享元类的公共方法。通过这些方法可以向外界提供享元对象的内部状态和设置外部状态

   2. 具体享元类 ConcreteFlyweight

  它实现了抽象享元类所声明的方法,其实例称为享元对象,为内部状态提供存储空间。

   3. 非共享具体享元类 UnSharedConcreteFlyWeight

   并不是所有抽象享元类的子类都需要被共享,不需要被共享的外部状态可设计为非共享具体享元类,它以参数的形式注入到具体享元的相关方法中,可以直接实例化。

   4. 享元工厂类 FlyWeightFactory

   用于创建和管理享元对象。

   它针对抽象享元类编程,将各种具体享元对象存储在一个享元池中。当用户请求一个具体享元对象时,享元工厂会检査系统中是否存在符合要求的享元对象,如果存在则提供给客户端,如果不存在,就创建一个新的享元对象。

   二、举例-联网类棋牌游戏

   一个游戏厅中有成千上万个“房间”,每个房间对应一个象棋棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。

     

   假设有 1w 场象棋游戏在同时进行,不使用享元模式的话,系统需要维护 32*1w 个象棋对象。但象棋的ID、文案、颜色、规则是不变的,变的只是持有人和位置。所以将32个象棋对象抽象出来,当做享元(共享的对象),可以极大的节省空间,而且不会带来成本提升。

   享元模式与其说是一种设计模式,不如说是一种设计理念,主要讲的是抽象的能力,将相同模块提取出来,供不同模块使用。从这个维度来说,代码重构中提取相同功能、单例模式等,何尝不是另一种享元。

  示例代码如下:

 1 // 抽象享元类:棋子单元
 2 public abstract class ChessPieceUnit {       
 3     //棋子类,有ID、文案、颜色,这三种不变属性
 4     private int id;
 5     private String text;
 6     private String color;
 7 
 8     public ChessPieceUnit(int id, String text, String color) {
 9         this.id = id;
10         this.text = text;
11         this.color = color;
12     }
13 
14     public abstract void display(int positionX, int positionY);
15 
16     // 省略其他方法和属性的实现
17 
18     public int getId() {
19         return id;
20     }
21 
22     public String getText() {
23         return text;
24     }
25 
26     public ChessPieceColor getColor() {
27         return color;
28     }
29 }
30 
31 // 具体享元类:具体棋子单元
32 public class ConcreteChessPieceUnit extends ChessPieceUnit {
33     public ConcreteChessPieceUnit(int id, String text, String color) {
34         super(id, text, color);
35     }
36 
37     @Override
38     public void display(int positionX, int positionY) {
39         System.out.println("棋子:" + getId() + " " + getColor() + " " + getText() + ",位置:" + positionX + ", " + positionY);
40     }
41 
42     // 省略特有的方法和属性的实现
43 }
44 
45 // 享元工厂类:棋子单元工厂
46 public class ChessPieceUnitFactory {
47     private static final Map<Integer, ChessPieceUnit> chessPieces = new HashMap<>();
48     // 通过工厂模式,在工厂类中,通过Map缓存已创建过的享元对象,达到复用
49     static {
50         chessPieces.put(1, new ConcreteChessPieceUnit(1, "将", "红"));
51         chessPieces.put(2, new ConcreteChessPieceUnit(2, "兵", "黑");
52         // 添加其他棋子
53     }
54 
55     public static ChessPieceUnit getChessPiece(int chessPieceId) {
56         return chessPieces.get(chessPieceId);
57     }
58 }
59 
60 // 客户端代码
61 public class Client {
62     public static void main(String[] args) {
63         ChessPieceUnit redGeneral = ChessPieceUnitFactory.getChessPiece(1);
64         redGeneral.display(0, 0);
65 
66         // 其他棋子的使用示例
67         ChessPieceUnit blackPawn = ChessPieceUnitFactory.getChessPiece(2);
68         blackPawn.display(1, 2);
69     }
70 }

   说明:

    1)ChessPieceUnit 抽象享元类

   在这个例子中,ChessPieceUnit 是抽象享元类,表示棋子单元。它包含了三种不变的属性:idtextcolor,这些属性是棋子的内部状态,因为它们是每个棋子实例都共享的,而且不随外部环境的变化而变化。

    2)display 方法

   display方法是一个抽象方法,表示展示棋子的操作。这个方法的参数 positionXpositionY 是外部状态,因为它们表示了棋子在棋盘上的具体位置,而这些位置信息可能因为外部环境的变化而变化。

   因此,ChessPieceUnit 类中的属性 idtextcolor 是内部状态,而 positionXpositionY 是外部状态。抽象享元类通过把内部状态和外部状态分离,使得多个享元对象可以共享相同的内部状态,而外部状态则可以在需要时进行传递,实现了享元模式的核心思想。

   3)  非共享具体享元类:

 1 // 非共享具体享元类:非共享的外部状态
 2 public class UnsharedConcreteChessPieceState {
 3     // 棋子类,有选中状态,这是可变属性
 4     private boolean isSelected;
 5 
 6     public UnsharedConcreteChessPieceState(boolean isSelected) {
 7         this.isSelected = isSelected;
 8     }
 9 
10     public boolean isSelected() {
11         return isSelected;
12     }
13 
14     public void setSelected(boolean selected) {
15         isSelected = selected;
16     }
17 }
18 
19 // 具体享元类:具体的棋子
20 public class ConcreteChessPieceUnit extends ChessPieceUnit {
21     private UnsharedConcreteChessPieceState unsharedState;
22 
23     public ConcreteChessPieceUnit(int id, String text, String color, UnsharedConcreteChessPieceState unsharedState) {
24         super(id, text, color);
25         this.unsharedState = unsharedState;
26     }
27 
28     @Override
29     public void display(int positionX, int positionY) {
30         System.out.println("棋子:" + getId() + " " + getColor() + " " + getText() + ",位置:" + positionX + ", " + positionY +
31                 ",选中状态:" + unsharedState.isSelected());
32     }
33 }

    在这个例子中,UnsharedConcreteChessPieceState 类只包含了选中状态,而坐标信息通过 display 方法的参数传递给具体享元类。这样,选中状态就成为了非共享的外部状态。

   三、内部状态和外部状态

    1.  内部状态 

    内部状态(Intrinsic State)是可以共享的,它独立于具体的享元对象,因此可以被多个对象共享。内部状态存储在享元对象内部,不会随着外部环境的改变而改变。

    比如上面例子中ChessPieceUnit 类中的属性 idtextcolor 就是内部状态

    2.  外部状态

    外部状态 (Extrinsic State)是不可共享的,它取决于具体的应用场景,并且随着外部环境的改变而改变。外部状态由客户端管理,而不是由享元对象管理。

    比如上面例子中ChessPieceUnit 类中 display() 方法中的形参:positionX 和 positionY,就是外部状态

    四、优缺点分析
     1. 优点

  •   通过共享对象减少了内存中对象的数量,降低了内存的占用,提高了系统的性能。
  •   享元模式的外部状态相对独立,不会影响其内部状态,从而使享元对象可以在不同的环境中被共享。

    2. 缺点

  •    需要分离出内部状态和外部状态,增加了系统的复杂性。
  •    为了使对象可以共享,需要将部分状态外部化,而读取外部状态会使运行时间变长。

   五、总结

    1.  享元模式这样理解:“享”就表示共享,“元”表示对象

    2. 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储

    3. 享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率

    4. 享元模式提高了系统的复杂度。需要分离出内部状态外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方

    5. 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制。

    6. 享元模式经典的应用场景是需要缓冲池的场景,比如 String常量池、数据库连接池

   六、JDK中享元模式的相关运用

   1.  Java的Integer类

   Integer使用享元模式复用对象。具体过程是通过IntegerCache缓存,返回共享的对象,以达到节省内存的目的。

   比如这段代码:

 1     public static void main(String[] args) {
 2         Integer i1 = 56;
 3         Integer i2 = 56;
 4         Integer i3 = 129;
 5         Integer i4 = 129;
 6         System.out.println(i1 == i2);
 7         System.out.println(i3 == i4);
 8         
 9         //返回结果
10         //true
11         //false
12     }

   因为Integer用了享元模式复用对象,才导致这样的运行差异。通过自动装箱,即调用valueOf()创建Integer对象时,如果要创建的Integer对象的值在-128到127之间,会从IntegerCache类中直接返回,否则才调用new方法创建:

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

  这里的IntegerCache相当于享元工厂类,只不过名字不叫xxxFactory而已,我们看下源码:

 1     private static class IntegerCache {
 2         static final int low = -128;
 3         static final int high;
 4         static final Integer cache[];
 5 
 6         static {
 7             // high value may be configured by property
 8             int h = 127;
 9             String integerCacheHighPropValue =
10                 sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
11             if (integerCacheHighPropValue != null) {
12                 try {
13                     int i = parseInt(integerCacheHighPropValue);
14                     i = Math.max(i, 127);
15                     // Maximum array size is Integer.MAX_VALUE
16                     h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
17                 } catch( NumberFormatException nfe) {
18                     // If the property cannot be parsed into an int, ignore it.
19                 }
20             }
21             high = h;
22 
23             cache = new Integer[(high - low) + 1];
24             int j = low;
25             for(int k = 0; k < cache.length; k++)
26                 cache[k] = new Integer(j++);
27 
28             // range [-128, 127] must be interned (JLS7 5.1.7)
29             assert IntegerCache.high >= 127;
30         }
31 
32         private IntegerCache() {}
33     }

     为什么IntegerCache只缓存-128到127之间的整型值呢?

     在IntegerCache的代码实现中,当这个类被加载的时候,缓存的享元对象会被集中一次性创建好。毕竟整型值太多了,我们不可能在IntegerCache类中预先创建好所有的整型值,这样既占用太多内存,也使得加载IntegerCache类的时间过长。所以,我们只能选择缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128到127之间的数据)。

     实际上,JDK也提供了方法来让我们可以自定义缓存的最大值,有下面两种方式。如果你通过分析应用的JVM内存占用情况,发现-128到255之间的数据占用的内存比较多,你就可以用如下方式,将缓存的最大值从127调整到255。不过,这里注意一下,JDK并没有提供设置最小值的方法。

1  //方法一:
2  -Djava.lang.Integer.IntegerCache.high=255
3  //方法二:
4  -XX:AutoBoxCacheMax=255
    需要注意的是,这个值如果设置得不当的话,可能会影响性能。
    实际上,除了Integer类型之外,其他包装器类型,比如Long、Short、Byte等,也都利用了享元模式来缓存-128到127之间的数据。

    2. String常量池

     比如这段代码:String s1 = "abc";

     跟Integer设计相似,String利用享元模式复用相同字符串常量(即“abc”)。JVM会专门开辟一块存储区来存储字符串常量,即“字符串常量池”

     不同版本的jdk中,字符串常量池所在的位置有所区别,如下图:

     1)jdk 7 之前

     在 Java 7 之前,字符串常量池位于永久代(Permanent Generation)的内存区域中,主要用来存储一些字符串常量(静态数据的一种)。永久代是 Java 堆(Java Heap)的一部分,用于存储类信息、方法信息、常量池信息等静态数据。

     2)jdk7

     从 Java 7 开始,为了解决永久代空间不足的问题,将字符串常量池从永久代中移动到堆中。这个改变也是为了更好地支持动态语言的运行时特性。

     3)jdk8

     到了 Java 8,永久代(PermGen)被取消,并由元空间(Metaspace)取代。元空间是一块本机内存区域,和 JVM 内存区域是分开的。元空间主要用于存储类的元数据,而字符串常量池和其他对象一样,存储在堆中。这种设计改进了内存管理,减少了由于字符串常量池导致的内存问题。

    跟Integer的不同点:

  • Integer类要共享对象,是在类加载时,一次性全部创建好
  • 字符串,没法预知要共享哪些字符串常量,所以无法事先创建只能在某字符串常量第一次被用到时,存储到常量池,再用到时,直接引用常量池中已存在的。

     

    参考链接:https://zhuanlan.zhihu.com/p/521739369

posted @ 2024-02-01 14:40  欢乐豆123  阅读(55)  评论(0编辑  收藏  举报