1.缘起:
对象池应该是一个“历史悠久”的概念了,像我们经常说的线程池、还有ADO.NET中的数据库连接池等,都属于对象池的应用。
我们的应用有时也会碰到需要使用对象池的情况,我举个例子说明一下。假设,我们需要记录某个类MyClass的每个方法每次被调用时方法执行所消耗的时间,而且,这个类是使用在多线程的环境中的,每个方法都可以同时在多个线程中执行,不需要被同步,这样可以使并发达到最大。
好,我们可以使用Stopwatch这个类来准确地记录每个方法的时间,关键是怎么使用它?为MyClass定义一个Stopwatch类型的成员变量,然后在每个方法的开始调用这个成员的Start以启动计时,在方法返回之前记录耗费的时间,然后Reset这个Stopwatch成员。
这种方案只有在一种情况下可以良好工作,那就是要求对MyClass的所有方法的调用都是同步的,而且多个线程调用同一个方法时也必须被同步,否则,Stopwatch的计时就会错乱了。
那么如何解决了?实际上很简单,我们不需要为MyClass定义一个Stopwatch类型的成员变量,而是在每个方法的入口处,new一个Stopwatch类型的局部变量,这个局部变量只服务于当前方法的一次调用。也就是说,对MyClass的任何一个方法的任何一次调用,都会产生一个Stopwatch类型的对象专门用于这次调用的计时工作,当这次调用返回后,该Stopwatch对象就可以被销毁了。这样就可以达到我们最初假设的需求。
从解决方案我们看到,每次调用都会新建一个Stopwatch对象,当调用返回时,这个对象就没有存在的价值而可以被销毁了。这样的结果就是会反复地创建并销毁Stopwatch类型的对象。
这正是使用对象池的一个绝佳场合,我们把一些可用的Stopwatch对象放进对象池中,每次方法被调用时,就向对象池租借一个Stopwatch对象来进行计时,调用返回时,再将这个对象归还给对象池即可。这样就避免了Stopwatch对象的重复创建和销毁。
我设计的ESBasic.ObjectManagement.Pool.IObjectPool就是一个通用的对象池,它是泛型的,所以可以池化存储不同类型的对象。
2.适用场合:
根据我们上面的描述,我们可以总结出当有类似以下的需求时,可以使用对象池技术。
(1)某个类型的对象经常被重复的创建、销毁。
(2)每个该类型的对象被使用的时间都很短。
(3)使用一个共享的对象无法达到系统的要求(比如会限制最大并发量)。
(4)相对于新建或销毁一个对象来说,清除对象的状态要容易得多。
3.设计思想与实现
IObjectPool接口的定义如下:
publicinterfaceIObjectPool<TObject>whereTObject:class
{
///
///MinObjectCount对象池中最少同时存在的对象数。
///
intMinObjectCount{get;set;}
///
///MaxObjectCount对象池中最多同时存在的对象数。
///
intMaxObjectCount{get;set;}
///
///DetectSpanInMSecs当池中没有空闲的对象且数量已达到MaxObjectCount时&#xff0c;如果这时发生Rent调用&#xff0c;则检测空闲对象的时间间隔。
///默认值为10ms。
///
intDetectSpanInMSecs{get;set;}
///
///PooledObjectCreator用于创建池中对象的创建器。默认为DefaultPooledObjectCreator
///
IPooledObjectCreator<TObject>PooledObjectCreator{set;}
voidInitialize();
TObjectRent();
voidGiveBack(TObjectobj);
}
这个接口有一个泛型参数&#xff1a;TObject&#xff0c;表示我们要池化的对象的类型。泛型约束表明能够放入对象池中的对象必须是引用类型。
MinObjectCount属性指示对象池在初始化的时候就必须确保池中存在的对象的数量。
MaxObjectCount属性表示对象池最多能够容纳的对象数量。
如果一个对象被租借出去&#xff0c;则对象池会将其状态标记为“繁忙”的&#xff1b;如果一个对象没有被租借出去或被归还&#xff0c;则其状况就是“空闲”的。
对于ObjectPool的实现&#xff0c;要注意以下几点&#xff1a;
&#xff08;1&#xff09;对象池是多线程安全的&#xff0c;可以在多线程的环境下使用。我们对内部的集合进行了加锁控制。
&#xff08;2&#xff09;对象池并不直接负责对象的创建工作&#xff0c;它把这项职责委托给了池化对象创建者IPooledObjectCreator。
&#xff08;3&#xff09;池化对象创建者IPooledObjectCreator不仅负责对象的创建工作&#xff0c;而且也负责清除对象的状态&#xff08;Reset方法&#xff09;。在GiveBack方法的内部就有调用Reset方法来清除对象的遗留状态的。IPooledObjectCreator接口的定义如下所示&#xff1a;
///
///IPooledObjectCreator池化对象创建者。用于创建被池缓存的对象。并能清除对象的状态。
///
publicinterfaceIPooledObjectCreator<TObject>whereTObject:class
{
TObjectCreate();
voidReset(TObjectobj);
}
4. 使用时的注意事项
&#xff08;1&#xff09;当外部调用Rent方法向对象池租借一个对象时&#xff0c;如果对象池中没有“空闲”的对象&#xff0c;并且池中的对象的数量已经达到了MaxObjectCount&#xff0c;那么这时该如何处理了&#xff1f;ObjectPool采用的策略是选择等待&#xff0c;等待直到有对象变成“空闲”&#xff0c;否则就一直阻塞当前线程。你必须注意到ObjectPool采用的这个策略可能会与你的期望不一致。
&#xff08;2&#xff09;当对象池中的空闲对象很多时&#xff0c;即使已经远远地大于了MinObjectCount的值&#xff0c;对象池也不会释放其中的某些对象&#xff0c;而是一直保持着。MinObjectCount只是决定了池在初始化的时候应该创建的对象的数量以备用。
&#xff08;3&#xff09;基于上面的两点原因&#xff0c;所以我们在具体应用时需要谨慎地为MaxObjectCount设定一个合理的值。如果这个值太小&#xff0c;可能会使得阻塞线程的情况经常发生。当然&#xff0c;这个值也不是设得越大越好&#xff0c;因为如果平时空闲的对象很多&#xff0c;就表示要占用更多的资源而却没有发挥出相应的价值。
&#xff08;4&#xff09;一个从池中借出的对象在被归还回给池的时候&#xff0c;必须把上次使用时遗留的状态清除掉&#xff0c;否则后面的租借者可能会误用其遗留的状态。
&#xff08;5&#xff09;如果清除一个对象的状态很不容易&#xff0c;相反创建和销毁一个对象却非常容易&#xff0c;那么这时可以考虑不使用对象池&#xff0c;而是每次new一个对象来使用&#xff0c;使用完了就丢弃掉。
5.扩展
如果我们要池化的对象是没有状态的&#xff0c;而且其类型也有不带参数的默认构造函数&#xff0c;那么我们可以直接使用ESBasic提供的默认池化对象创建者DefaultPooledObjectCreator。DefaultPooledObjectCreator使用反射创建对象&#xff0c;并且Reset方法也不对对象做任何供动作――因为对象本身就是没有状态的&#xff0c;所以也就不存在清除其状态的需要了。
注&#xff1a;ESBasic源码可到http://esbasic.codeplex.com/下载。
ESBasic讨论&#xff1a;37677395
ESBasic开源前言