当我们在系统用到某些占用内存较多的大对象,且该对象并不会被频繁使用(例如缓存场景)时,若考虑性能因素,或许我们可以选择使用弱引用(WeakReference)对象。弱引用对象就像是对象之中的“无间行者”,行走于“活动”与“非活动”状态之间。即使该对象存在引用,垃圾回收器仍然可以对其进行回收,这使得我们对该对象的调用始终存在一种不可预知性,除非我们通过Target属性赋给对象,以创建强引用,否则我们始终处于这种忧虑之中。这让我们常常感到左右为难,但在一些追求性能的场景下,使用弱引用未尝不是明智的选择。只要我们遵循一定的原则,例如在每次调用弱引用对象时,首先判断其是否为null,就不会存在太大的问题(如果考虑并发,则需要lock,通常需要做两次对null的判断,就如在Singleton模式中对并发支持的实现一样)。然而,当我们在一个集合对象中存储弱引用对象时,问题就出现了意想不到的变化。
首先是对集合Count属性的判断。如果一个集合对象在某个时刻存储了10个弱引用对象,当我们调用该集合的Count属性时,返回的值应该是多少?很显然,我们不能做预先的判断。事实上,因为弱引用对象的存在,一个本身线程安全的调用却出现了并发问题。因为在调用Count属性期间,GC正有可能回收集合中的某些元素对象。
其次是对迭代器的支持。我们知道,在对IEnumerable进行Foreach遍历时,不允许我们Add或Remove集合的元素,否则遍历就会失败。如果没有弱引用对象,一切都是美好的。但在我们遍历存储了弱引用对象的集合时,GC就会像幽灵一般,不知什么时候钻出来捣乱,改变集合元素的个数。很显然,这样的集合对迭代器的支持是不安全的。
实质上,这两个问题的本源是相同的,起因就是弱引用的特殊性。如何解决这个问题?一个简单办法是定义自己的弱引用集合,对集合对象进行一个简单的Wrapper,同时隐藏Count属性,以及其对IEnumerable的支持。例如,定义一个弱引用列表:
public sealed class WeakReferenceList<T>{
private List<WeakReference> m_list;
public void Add(T object)
{
//这里使用短弱引用,一般不建议使用长弱引用;
m_list.Add(new WeakReference(object));
}
//其它成员
}
WeakReferenceList类并没有提供GetEnumerator()方法的必要,原因如前所述。至于Count属性,我们是否可以通过查询条件,以过滤那些值为null的对象呢?例如:
public int Count{
get
{
return m_list.Count(e => e.IsAlive);
}
}
表面看来这是可行的,可是彼时返回的值,在使用该值的时候未必正确,即使对其进行lock,也无法保证其正确性。与其如此,还不如不提供Count属性。
剩下一个问题是如何获得WeakReferenceList的元素?我们需要为其实现特有的索引器。例如:
public T this[int index]{
get
{
//这里仍然使用了Count属性(这说明我们可以定义一个私有属性Count)
if (index < 0 || index >= this.Count)
{
throw new IndexOutOfRangeException();
}
else
{
if (m_list[index].Target != null)
{
return (T)m_list[index].Target;
}
else
{
throw new Exception("The object is collected by GC.")
}
}
}
}
private int Count
{
get
{
return m_list.Count(e => e.IsAlive);
}
}
更好地管理弱引用对象的集合是HashTable,这也是缓存的一种好的存储机制。关于HashTable对弱引用对象的应用,请参见Jared Parsons的文章Building a WeakReference Hashtable。实际上,本文正是借鉴了该文的思想。
本文出自 “晴窗笔记” 博客,请务必保留此出处http://wayfarer.blog.51cto.com/1300239/280097