设计模式
设计模式简介
看懂UML类图和时序图
UML统一建模语言
UML类图及类图之间的关系
类关系记忆技巧
如何正确使用设计模式
优秀设计的特征
面向对象设计原则
创建型设计模式
工厂模式
抽象工厂模式
简单工厂模式
静态工厂模式(Static Factory)
单例模式
建造者模式
原型模式
结构型设计模式
适配器模式
桥接模式
组合模式
装饰器模式
外观模式
享元模式
代理模式
过滤器模式
注册模式(Registry)
行为型设计模式
责任链模式
命令模式
解释器模式
中介者模式
备忘录模式
迭代器模式
观察者模式
状态模式
策略模式
模板模式
访问者模式
规格模式(Specification)
J2EE 设计模式
MVC 模式
业务代表模式
组合实体模式
数据访问对象模式(DAO模式)
前端控制器模式
拦截过滤器模式
空对象模式
服务定位器模式
传输对象模式
数据映射模式(Data Mapper)
依赖注入模式(Dependency Injection)
流接口模式(Fluent Interface)
其他模式
对象池模式(Pool)
委托模式
资源库模式(Repository)
实体属性值模式(EAV 模式)
反面模式
归纳设计模式
本文档使用 MrDoc 发布
-
+
首页
对象池模式(Pool)
> 放弃单独地分配和释放对象,从固定的池中重用对象,以提高性能和内存使用率。 定义一个池对象,其包含了一组可重用对象。 其中每个可重用对象都支持查询“使用中”状态,说明它是不是“正在使用”。 池被初始化时,它就创建了整个对象集合(通常使用一次连续的分配),然后初始化所有对象到“不在使用中”状态。 当你需要新对象,向池子要一个。 它找到一个可用对象,初始化为“使用中”然后返回。 当对象不再被需要,它被设置回“不在使用中”。 通过这种方式,可以轻易地创建和销毁对象而不必分配内存或其他资源。 ## 目的 对象池模式是一种提前准备了一组已经初始化了的对象『池』而不是按需创建或者销毁的创建型设计模式。对象池的客户端会向对象池中请求一个对象,然后使用这个返回的对象执行相关操作。当客户端使用完毕,它将把这个特定类型的工厂对象返回给对象池,而不是销毁掉这个对象。 在初始化实例成本高,实例化率高,可用实例不足的情况下,对象池可以极大地**提升性能**。在创建对象(尤其是通过网络)时间花销不确定的情况下,通过对象池在可期时间内就可以获得所需的对象。 无论如何,对象池模式在需要耗时创建对象方面,例如创建数据库连接,套接字连接,线程和大型图形对象(比方字体或位图等),使用起来都是大有裨益的。在某些情况下,**简单的对象池(无外部资源,只占内存)可能效率不高,甚至会有损性能**。 ### 碎片空间 碎片意味着在堆中的空余空间被打碎成了很多小的内存碎片,而不是大的连续内存块。 总共的 可用内存也许很大,但是最长的连续空间可能难以忍受地小。 假设我们有十四个空余字节,但是被一块正在使用的内存分割成了两个七字节的碎片。 而我们尝试分配十二字节的对象,那么就会失败。 ![一系列导致碎片化的内存操作](/media/202203/2022-03-20_2146010.07656724485823874.png) ## UML ![对象池模式](/media/202203/2022-03-20_2136270.4456886135542235.png) ## 使用场景 在以下情况中使用对象池: * 需要频繁创建和销毁对象。 * 对象大小相仿。 * **在堆上进行对象内存分配十分缓慢或者会导致内存碎片**。 * **每个对象都封装了像数据库或者网络连接这样很昂贵又可以重用的资源**。 ## 注意事项 - 池可能在不需要的对象上浪费内存 对象池的大小需要根据需求设置。 当池子太小时,很明显需要调整。 但是也要小心确保池子没有太大。,更小的池子提供了空余的内存做其他事情。 - 同时只能激活固定数量的对象 在某种程度上这是好事。 将内存按不同的对象类型划分单独的池保证了这点。 尽管如此,这也意味着试图从池子重用对象可能会失败,因为它们都在使用中。 这里有几个常见对策: - 完全阻止这点。 这是通常的“修复”:增加对象池的大小,这样无论用户做什么,它们都不会溢出。 对于重要对象,这通常是正确的选择。 - 这个的副作用是强迫你为那些只在一两个罕见情况下需要的对象分配过多的内存。 因此,固定大小的对象池也许不对所有的情况都适用。在这种情况下,考虑为每个场景调整对象池的大小。 - 就不要创建对象了。 这听起来很糟,但是对于像粒子系统这样的情况很有道理。 如果所有的粒子都在使用,那么屏幕已经充满了闪动的图形。 用户不会注意到下个爆炸不如现在的这个一样引人注目。 - 强制干掉一个已有的对象。大体上,如果已有对象的消失要比新对象的出现更不引人察觉,这也许是正确的选择。 - 增加池的大小。 如果允许你使用一点内存上的灵活性,我们也许会在运行时增加池子的大小或者创建新的溢出池。 如果用这种方式获取内存,考虑下在增加的内存不再需要时,池是否需要缩回原来的大小。 - 每个对象的内存大小是固定的 多数对象池将对象存储在一个数组中。 如果你所有的对象都是同样的类型,这很好。 但是,如果你想要在同一个对象池中存储不同类型的对象,或者存储子类的实例, 你需要保证池中的每个位置对最大的可能对象都有足够的内存。 否则,超过预期大小的对象会占据下一个对象的内存空间,导致内存崩坏。 同时,如果对象大小是变化的,你是在浪费内存。 每个槽都需要能存储最大的对象。 如果对象很少那么大,每放进去一个小对象都是在浪费内存。当你发现自己在用这种方式浪费内存,考虑将池根据对象的大小分割为分离的池。这是一种实现有效率的内存管理的常用模式。 管理者拥有一系列池,池的块大小不相同。 当你申请分配一块,它会从合适块大小的池中取出一块,然后分配给你。 - 重用对象不会自动清除。 由于对象池重用对象不再经过内存管理系统,我们失去了这层安全网。 更糟的是,为“新”对象使用的内存之前存储的是同样类型的对象。 这使你很难分辨出创建新对象时的未初始化问题: 那个存储新对象的内存已经保存了来自于上个生命周期中的几乎完全正确的数据。 由于这点,特别注意在池里初始化对象的代码,保证它完全地初始化了对象。 - 未使用的对象会保留在内存中 对象池在支持垃圾回收的系统中很少见,因为内存管理系统通常会为你处理这些碎片。 但是对象池仍然是避免构建和析构的有用手段,特别是在有更慢CPU和更简陋垃圾回收系统的移动设备上。 如果你使用有垃圾回收的对象池系统,注意潜在的冲突。 由于池不会在对象不再使用时真正地析构它们,如果对象仍然保留任何对其他对象的引用,也会阻止垃圾回收器回收它。 为了避免这点,当池中对象不再使用,清除它对其他对象的所有引用。 ## 设计决策 如你所见,对象池最简单的实现非常平凡:创建对象数组,在需要它们时重新初始化。 实际的代码很少会那么简单,这里还有很多方式让池更加的通用,安全,或容易管理。 ### 对象和池耦合吗? 写对象池时第一个需要思考的问题:对象本身是否需要知道它们在池子中。 大多数情况下它们需要,但是那样你就不大可能写一个通用对象池类来保存任意对象。 * **如果对象与池耦合:** * *实现更简单。* 你可以在对象中简单地放个“在使用中”标识或者函数,就完成了。 * **你可以保证对象只能被对象池创建。** 在 C++ 中,做这事最简单的方法是让池对象是对象类的友类,将对象的构造器设为私有。 ```cpp class Particle { friend class ParticlePool; private: Particle() : inUse_(false) {} bool inUse_; }; class ParticlePool { Particle pool_[100]; }; ``` 在类间保持这种关系来确保使用者无法创建对象池没有追踪的对象。 * 你也许**可以避免显式存储“使用中”的标识**。很多对象已经保存了可以告诉外界它有没有在使用的状态。 举个例子,粒子的位置如果不在屏幕上,也许它就可以被重用。 如果对象类知道它在对象池中,那它可以提供一个 `inUse()` 来查询这个状态。 这省下了对象池存储“在使用中”标识的多余内存。 * **如果对象没有和对象池耦合:** * **可以保存多种类型的对象。** 这是最大的好处。通过解耦对象和对象池,你可以实现通用的、可重用的对象池类。 * **必须在对象的外部追踪“使用中”状态。** 做这点最简单的方式是创建分离的位字段: ```cpp template <class TObject> class GenericPool { private: static const int POOL_SIZE = 100; TObject pool_[POOL_SIZE]; bool inUse_[POOL_SIZE]; }; ``` ### 谁负责初始化重用对象? 为了重用一个已经存在的对象,它必须用新状态重新初始化。 这里的关键问题是你需要在对象池的内部还是外部重新初始化。 * **如果在对象池的内部重新初始化:** * *对象池可以完全封装管理对象。* 取决于对象需要的其他能力,你可以让它们完全处于池的内部。 这保证了其外部代码不会引用到已重用的对象。 * *对象池与对象是如何初始化的相绑定。* 池中对象也许提供了不同的初始化函数。 如果对象池控制了初始化,它的接口需要支持所有的初始化函数,然后转发给对象。 ```cpp class Particle { // 多种初始化方式…… void init(double x, double y); void init(double x, double y, double angle); void init(double x, double y, double xVel, double yVel); }; class ParticlePool { public: void create(double x, double y) { // 转发给粒子…… } void create(double x, double y, double angle) { // 转发给粒子…… } void create(double x, double y, double xVel, double yVel) { // 转发给粒子…… } }; ``` * **如果外部代码初始化对象:** * *对象池的接口更简单。* 无需提供覆盖每种对象初始化的多种函数,对象池只需要返回新对象的引用: ```cpp class Particle { public: // 多种初始化方法 void init(double x, double y); void init(double x, double y, double angle); void init(double x, double y, double xVel, double yVel); }; class ParticlePool { public: Particle* create() { // 返回可用粒子的引用…… } private: Particle pool_[100]; }; ``` 调用者可以使用对象暴露的任何方法进行初始化: ```cpp ParticlePool pool; pool.create()->init(1, 2); pool.create()->init(1, 2, 0.3); pool.create()->init(1, 2, 3.3, 4.4); ``` * *外部代码需要处理无法创建新对象的失败。* 前面的例子假设 `create()` 总能成功地返回一个指向对象的指针。 但如果对象池已经满了,返回的会是 `NULL`。安全起见,你需要在初始化之前检查这一点。 ```cpp Particle* particle = pool.create(); if (particle != NULL) particle->init(1, 2) ``` ## 与其他模式的关系 * 这看上去很像是享元模式。 两者都控制了一系列可重用的对象。不同在于“重用”的含义。 享元对象分享实例间*同时*拥有的相同部分。享元模式在不同上下文中使用相同对象避免了重复内存使用。 对象池中的对象也被重用了,但是是在不同的时间点上被重用的。 “重用”在对象池中意味着对象在原先的对象用完之后分配内存。 对象池没有期待对象会在它的生命周期中分享什么。 * 将内存中同样类型的对象进行整合,能确保在遍历对象时 CPU 缓存总是满的。 数据局部性模式介绍了这一点。 ## 示例代码 ### PHP WorkerPool.php ```php <?php namespace DesignPatterns\Creational\Pool; class WorkerPool implements \Countable { /** * @var StringReverseWorker[] */ private $occupiedWorkers = []; /** * @var StringReverseWorker[] */ private $freeWorkers = []; public function get(): StringReverseWorker { if (count($this->freeWorkers) == 0) { $worker = new StringReverseWorker(); } else { $worker = array_pop($this->freeWorkers); } $this->occupiedWorkers[spl_object_hash($worker)] = $worker; return $worker; } public function dispose(StringReverseWorker $worker) { $key = spl_object_hash($worker); if (isset($this->occupiedWorkers[$key])) { unset($this->occupiedWorkers[$key]); $this->freeWorkers[$key] = $worker; } } public function count(): int { return count($this->occupiedWorkers) + count($this->freeWorkers); } } ``` StringReverseWorker.php ```php <?php namespace DesignPatterns\Creational\Pool; class StringReverseWorker { /** * @var \DateTime */ private $createdAt; public function __construct() { $this->createdAt = new \DateTime(); } public function run(string $text) { return strrev($text); } } ``` Tests/PoolTest.php ```php <?php namespace DesignPatterns\Creational\Pool\Tests; use DesignPatterns\Creational\Pool\WorkerPool; use PHPUnit\Framework\TestCase; class PoolTest extends TestCase { public function testCanGetNewInstancesWithGet() { $pool = new WorkerPool(); $worker1 = $pool->get(); $worker2 = $pool->get(); $this->assertCount(2, $pool); $this->assertNotSame($worker1, $worker2); } public function testCanGetSameInstanceTwiceWhenDisposingItFirst() { $pool = new WorkerPool(); $worker1 = $pool->get(); $pool->dispose($worker1); $worker2 = $pool->get(); $this->assertCount(1, $pool); $this->assertSame($worker1, $worker2); } } ```
追风者
2022年3月20日 22:22
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
关于 MrDoc
觅思文档MrDoc
是
州的先生
开发并开源的在线文档系统,其适合作为个人和小型团队的云笔记、文档和知识库管理工具。
如果觅思文档给你或你的团队带来了帮助,欢迎对作者进行一些打赏捐助,这将有力支持作者持续投入精力更新和维护觅思文档,感谢你的捐助!
>>>捐助鸣谢列表
微信
支付宝
QQ
PayPal
Markdown文件
分享
链接
类型
密码
更新密码