内功心法专题-设计模式-13、享元模式
1. 模式定义⭐️🔴
- 1)享元模式(Flyweight Pattern)也叫 蝇量模式:运用共享技术有效地支持大量细粒度的对象
- 2)常用于系统底层开发,==解决系统的性能问题==。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
- 3)享元模式能够==解决重复对象的内存浪费的问题==。当系统中有大量相似对象,需要缓冲池时,不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
- 4)享元模式经典的应用场景就是池技术了,String 常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式
2. 模式结构
2.1. 两种状态
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,存储在享元对象内部且不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点。所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同。当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态
举个例子:围模理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生。因为内存空间有限,一台服务器很难支持更多的玩家玩围模游戏。如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例,这样就很好的解决了对象的开销问题
2.2. 角色
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中==声明了具体享元类公共的方法==,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight) 角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
2.3. UML 图示⭐️🔴
2.4. 实现逻辑⭐️🔴
享元工厂聚合抽象享元,作为 HashMap<String,AbstractBox>
这个池子的 Value 属性,将具体享元对象放入这个享元池中。方便获取并能节省空间,提高性能。
❕
3. 案例分析⭐️🔴
❕
3.1. 俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
享元模式 UML
AbstractBox:抽象享元角色,俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出 AbstractBox,用来定义共性的属性和行为。
IBox、LBox、OBox:具体享元角色
BoxFactory:享元工厂角色,用来管理享元对象(也就是 AbstractBox 子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。
外部状态:各种图形的 shape,数据不共享
内部状态:client 调用时指定的颜色是不会再变化,是不同 shape 共享的数据
1 |
|
1 |
|
示例代码:[[pages/002-schdule/001-Arch/001-Subject/013-DemoCode/design_patterns/src/main/java/com/itheima/pattern/flyweight/BoxFactory.java]]
3.2. 小网站多用户
内部状态:网站可共享的部分,不需要个性化的数据
外部状态:User
1 |
|
示例代码:[[WebSiteFactory.java]]
4. 优缺点⭐️🔴
优点
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
缺点
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,并且需要有一个工厂类加以控制,使程序逻辑复杂
- 用享元模式需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此应当在需要多次重复使用享元对象时才值得使用享元模式。
4.1. 外部化
比如俄罗斯方块案例中,颜色是在 client 调用时传入的,破坏了享元类的封装性
5. 适用场景⭐️🔴
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
6. JDK 源码分析
6.1. Integer
Integer 类使用了享元模式。我们先看下面的例子:
1 |
|
上面代码可以看到,直接给 Integer 类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf()
,所以只需要看该方法即可
1 |
|
1 |
|
可以看到 Integer
默认先创建并缓存 -128 ~ 127
之间数的 Integer
对象,当调用 valueOf
时如果参数在 -128 ~ 127
之间则计算下标并从缓存中返回,否则创建一个新的 Integer
对象。
❕
7. 享元模式与其他模式技术区别⭐️🔴
7.1. VS 单例模式
- 在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。
- 应用享元模式是为了对象复用,节省内存,而应用单例多例模式是为了限制对象的个数。
7.2. VS 池化技术
- 池化技术中的“复用”可以理解为“重复的独占使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
- 享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
7.3. VS 缓存
- 应用单例模式是为了保证对象全局唯一。应用享元模式是为了实现对象复用,节省内存。缓存是为了提高访问效率,而非复用。
8. 实战经验
- 1)在享元模式这样理解,“享”就表示共享,“元”表示对象
- 2)系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
- 3)用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用 HashMap/HashTable 存储
- 4)享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率
- 5)享元模式提高了系统的复杂度,需要分离出内部状态和外部状态。而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方
- 6)使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
- 7)享元模式经典的应用场景是需要缓冲池的场景,比如 String 常量池、数据库连接池
8.1. 线程安全问题
8.1.1. 不可变性
在我们的代码中,经常要传递容器类的对象,比如 Map,Set, List 等。 在这样的传递中,通常很少考虑不可变性。 作为应用的开发者, 这样写问题不大。 但是作为框架的开发者,提供 library 给外部用户, 如果不考虑这些问题通常就会导致一些问题。比如返回一个 HashMap, 如果这个对象在遍历的时候,有新的对象插入就会有并发的问题。 那么这个 HashMap 到底希望拿到这个对象的用户修改,还是不希望他们修改。
如果不希望修改,那么就应该做成 immutable 的, 首先不会出现上文提到的并发调用的冲突问题,其实 immutalbe 的对象是不用考虑并发的问题的,它是天然线程安全的。
如果希望修改,通常就考虑返回线程安全的容器,比如 ConcurrentHashMap 之类。
链接: https://www.jianshu.com/p/b9cb755b0ad9
https://bbs.huaweicloud.com/blogs/325161
#todo