享元模式在Apache Pool源码中的应用

结构型模式                 ————顺口溜:适装桥组享代外

目录

1、享元模式

1.1 享元模式UML图

1.2 日常生活中看享元模式

1.3 应用实例

1.4 Java代码实现

2、享元模式在Apache Pool源码中的应用

2.1 核心对象

2.1.1 PooledObject(池对象)

2.1.2 PooledObjectFactory(池对象工厂)

2.1.3 Object Pool(对象池)

2.2 核心流程

2.2.1 BorrowObject (借出对象)

2.2.2 BorrowObject (返回对象)

3、享元模式的优缺点

3.1 优点

3.2 缺点

3.3 使用场景

3.4 注意事项

4、享元模式与单例模式的异同


1、享元模式

享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。

说到享元模式,第一个想到的应该就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,所以说享元模式是池技术的重要实现方式。

比如我们每次创建字符串对象时,都需要创建一个新的字符串对象的话,内存开销会很大,所以如果第一次创建了字符串对象“adam“,下次再创建相同的字符串”adam“时,只是把它的引用指向”adam“,这样就实现了”adam“字符串再内存中的共享。

  • 意图:运用共享技术有效地支持大量细粒度的对象。
  • 主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
  • 何时使用: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
  • 如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
  • 关键代码:用 HashMap 存储这些对象。

1.1 享元模式UML图

1.2 日常生活中看享元模式

       文书处理器中以图形结构来表示字bai符。一个做法是,每个字形有其字型外观, 字模 metrics, 和其它格式资讯,但这会使每个字符就耗用上千字节。取而代之的是,每个字符参照到一个共享字形物件,此物件会被其它有共同特质的字符所分享;只有每个字符(文件中或页面中)的位置才需要另外储存。

  这里再举一个实例,比如接了我一个小型的外包项目,是做一个产品展示网站,后来他的朋友们也希望做这样的网站,但要求都有些不同,我们当然不能直接复制粘贴再来一份,有任希望是新闻发布形式的,有人希望是博客形式的等等,而且因为经费原因不能每个网站租用一个空间。其实这里他们需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,这是造成服务器的大量资源浪费。如果整合到一个网站中,共享其相关的代码和数据,那么对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源;而对于代码,由于是一份实例,维护和扩展都更加容易。——引自《大话设计模式》

1.3 应用实例

  1. JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。
  2. 数据库的数据池。

1.4 Java代码实现

       举一个例子来说明,在一个象棋游戏中,每个对局都会有一个棋局对象,同时在线可能会有成千上万的棋局对象,每个棋局又有多个对象,那就需要数万乃至数十万的棋子对象,这样就会对内存造成极大的消耗,影响程序性能。但是在每个棋局中的棋子的种类、颜色等属性都是固定不变的,不同的两个棋局中的棋子对象的区别就只是属于不同的棋局和拥有不同坐标,因此可以创建一个模板棋局,其中包含所有的棋子对象,将这个对象定义成一个静态变量,以后新增的棋局对象都可以使用这个棋局对象作为模板,共享使用这个棋局和其中的棋子对象,只需要在不同的棋局中修改棋子的坐标即可。代码如下:

//棋子享元类,不同棋盘中不同坐标的棋子的相同的属性对象。
public class ChessUnit {
	public int id;
	public String color;
	public String chessName;
	
	public ChessUnit (int id, String color, String chessName) {
		this.id = id;
		this.color = color;
		this.chessName = chessName;
	}
}
//真正的棋子类
public class Chess {
	private ChessUnit chessUnit;
	private int x;
	private int y;
	public Chess(ChessUnit chessUnit, int x, int y){
		this.chessUnit = chessUnit;
		this.x = x;
		this.y = y;
	}

}
//提供一个工厂类,保存不变的那些固定的不变的要被共享的享元对象,用静态对象保存
public class ChessUnitFactory {
	
	private static final  Map<Integer, ChessUnit>  chesses = new HashMap<Integer, ChessUnit>();
	
	static {
		chesses.put(1, new ChessUnit(1, "red", "马"));
		chesses.put(2, new ChessUnit(1, "red", "将"));
		chesses.put(3, new ChessUnit(1, "red", "士"));
		chesses.put(4, new ChessUnit(1, "red", "象"));	
	}
	public static ChessUnit getChessByid(int chessId){
		return chesses.get(chessId);
	}

}
//一个棋盘类,在构造方法中调用init方法,利用保存好的静态变量来初始化对象,节约内存空间。
public class ChessBoard {
	
	private Map<Integer, Chess> chessBoard = new HashMap<Integer, Chess>();
	
	public	ChessBoard(){
		init();
	}
	
	public void init() {
		chessBoard.put(1, new Chess(ChessUnitFactory.getChessByid(1),123,32));
		chessBoard.put(2, new Chess(ChessUnitFactory.getChessByid(2),123,32));
		chessBoard.put(3, new Chess(ChessUnitFactory.getChessByid(3),123,32));
		chessBoard.put(4, new Chess(ChessUnitFactory.getChessByid(4),123,32));
	}
}

2、享元模式在Apache Pool源码中的应用

Commons Pool2本质上是”对象池”,即通过一定的规则来维护对象集合的容器;commos-pool2在很多场景中,用来实现”连接池”/”任务worker池”等,大家常用的Jedis pool数据库连接池,也是基于commons-pool2实现.

  commons-pool2实现思想非常简单,它主要的作用就是将"对象集合"池化,任何通过pool进行对象存取的操作,都会严格按照"pool配置"(比如池的大小)实时的"建对象"、"阻塞控制"、"销毁对象"等.它在一定程度上,实现了对象集合的管理以及对象的分发.

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

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

  • 1 :将创建对象的方式,使用工厂模式;
  • 2 :通过”pool配置”来约束对象存取的时机;
  • 3 :将对象列表保存在队列中(LinkedList)

2.1 核心对象

在讲述其实现原理前,先提一下其中有几个重要的对象:
- PooledObject(池对象)。
- PooledObjectFactory(池对象工厂)。
- Object Pool(对象池)。

2.1.1 PooledObject(池对象)

用于封装对象(如:线程、数据库连接、TCP连接),将其包裹成可被池管理的对象。提供了两个默认的池对象实现:
- DefaultPoolObject。用于非软引用的普通对象。
- PooledSoftReference。用于软引用的对象。
在开发连接池、线程池等组件时,需要根据实际情况重载5个方法:startEvictionTest、endEvictionTest、allocate、deallocate和invalidate,用于在不同的场景下修改被包裹对象的内部状态。

这里写图片描述

这里写图片描述

PooledObject有多种状态,在不同的环节或经过处理后状态会发生变化:

状态描述
IDLE位于队列中,未使用
ALLOCATED在使用
EVICTION位于队列中,当前正在测试,可能会被回收
EVICTION_RETURN_TO_HEAD不在队列中,当前正在测试,可能会被回收。从池中借出对象时需要从队列出移除并进行测试
VALIDATION位于队列中,当前正在验证
VALIDATION_PREALLOCATION不在队列中,当前正在验证。当对象从池中被借出,在配置了testOnBorrow的情况下,对像从队列移除和进行预分配的时候会进行验证
VALIDATION_RETURN_TO_HEAD不在队列中,正在进行验证。从池中借出对象时,从队列移除对象时会先进行测试。返回到队列头部的时候应该做一次完整的验证
INVALID回收或验证失败,将销毁
ABANDONED即将无效
RETURN

返还到池中

2.1.2 PooledObjectFactory(池对象工厂)

定义了操作PooledObject实例生命周期的一些方法,PooledObjectFactory必须实现线程安全。已经有两个抽象工厂:
- BasePooledObjectFactory。
- BaseKeyedPooledObjectFactory。

这里写图片描述

状态描述
makeObject用于生成一个新的ObjectPool实例
activateObject每一个钝化(passivated)的ObjectPool实例从池中借出(borrowed)前调用
validateObject可能用于从池中借出对象时,对处于激活(activated)状态的ObjectPool实例进行测试确保它是有效的。也有可能在ObjectPool实例返还池中进行钝化前调用进行测试是否有效。它只对处于激活状态的实例调用
passivateObject当ObjectPool实例返还池中的时候调用
destroyObject当ObjectPool实例从池中被清理出去丢弃的时候调用(是否根据validateObject的测试结果由具体的实现在而定)

2.1.3 Object Pool(对象池)

Object Pool负责管理PooledObject,如:借出对象,返回对象,校验对象,有多少激活对象,有多少空闲对象。有三个默认的实现类:
- GenericObjectPool。
- ProsiedObjectPool。
- SoftReferenceObjectPool。

这里写图片描述

状态描述
borrowObject从池中借出一个对象。要么调用PooledObjectFactory.makeObject方法创建,要么对一个空闲对象使用PooledObjectFactory.activeObject进行激活,然后使用PooledObjectFactory.validateObject方法进行验证后再返回
returnObject将一个对象返还给池。根据约定:对象必须 是使用borrowObject方法从池中借出的
invalidateObject废弃一个对象。根据约定:对象必须 是使用borrowObject方法从池中借出的。通常在对象发生了异常或其他问题时使用此方法废弃它
addObject使用工厂创建一个对象,钝化并且将它放入空闲对象池
getNumberIdle返回池中空闲的对象数量。有可能是池中可供借出对象的近似值。如果这个信息无效,返回一个负数
getNumActive返回从借出的对象数量。如果这个信息不可用,返回一个负数
clear清除池中的所有空闲对象,释放其关联的资源(可选)。清除空闲对象必须使用PooledObjectFactory.destroyObject方法
close关闭池并释放关联的资源

2.2 核心流程

2.2.1 BorrowObject (借出对象)

下面是GenericObjectPool中borrowObject方法的逻辑实现,有阻塞式和非阻塞式两种获取对象的模式。

这里写图片描述

2.2.2 BorrowObject (返回对象)

下面是GenericObjectPool中returnObject方法的逻辑实现,在这里实现的FIFO(先进先出)和LIFO(后进先出)。

这里写图片描述

3、享元模式的优缺点

3.1 优点

  • 大大减少对象的创建
  • 降低系统的内存
  • 使效率提高

3.2 缺点

提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱

3.3 使用场景

  1. 系统有大量相似对象。
  2. 需要缓冲池的场景。

3.4 注意事项

  1. 注意划分外部状态和内部状态,否则可能会引起线程安全问题。
  2. 这些类必须有一个工厂对象加以控制

4、享元模式与单例模式的异同

单例模式和享元模式都是为了避免重复创建对象,但是其本质是不一样的:

  1. 其实现方式不一样,单例是一个类只有一个唯一的实例,而享元可以有多个实例,只是通过一个共享容器来存储不同的对象。
  2. 使用场景不一样,单例是强调减少实例化提升性能,因此一般用于一些需要频繁创建和销毁实例化对象或创建和销毁实例化对象非常消耗资源的类中,如连接池、线程池。而享元则是强调共享相同对象或对象属性,节约内存使用空间。

参考文章:

https://blog.youkuaiyun.com/weixin_37486553/article/details/108818848

https://blog.youkuaiyun.com/amon1991/article/details/77110657

https://blog.youkuaiyun.com/zilong_zilong/article/details/78556281

https://blog.youkuaiyun.com/weixin_43871678/article/details/107634401

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值