在设计具有对另一个对象的引用的类时,仅在第一次使用时创建引用的对象可能是有益的,例如,使用延迟加载。
我经常使用这种模式来创建一个延迟加载的属性:
Encoding utf8NoBomEncoding;
Encoding Utf8NoBomEncoding {
get {
return this.utf8NoBomEncoding ??
(this.utf8NoBomEncoding = new UTF8Encoding(false));
}
}
然后我在浏览BCL的源代码时遇到了这段代码:
Encoding Utf8NoBomEncoding {
get {
if (this.utf8NoBomEncoding == null) {
var encoding = new UTF8Encoding(false);
Thread.MemoryBarrier();
this.utf8NoBomEncoding = encoding;
}
return this.utf8NoBomEncoding;
}
}
据我所知,这些都不是线程安全的。例如。可以创建多个Encoding
个对象。我完全明白了,如果创建了一个额外的Encoding
对象,那就不是问题了。它是不可变的,很快就会被垃圾收集。
但是,我真的很想知道为什么Thread.MemoryBarrier
是必要的,以及第二种实现与多线程场景中的第一种实现有何不同。
显然,如果线程安全是一个问题,最好的实现可能是使用Lazy<T>
:
Lazy<Encoding> lazyUtf8NoBomEncoding =
new Lazy<Encoding>(() => new UTF8Encoding(false));
Encoding Utf8NoBomEncoding {
get {
return this.lazyUtf8NoBomEncoding.Value;
}
}
答案 0 :(得分:6)
这段代码将是一场没有内存障碍的灾难。仔细看看这些代码行。
var encoding = new UTF8Encoding(false);
Thread.MemoryBarrier();
this.utf8NoBomEncoding = encoding;
现在,想象一下其他一些线程看到最后一行的效果但看不到第一行的效果。那将是一场彻底的灾难。
内存屏障确保任何看到encoding
的线程都能看到其构造函数的所有效果。
例如,在没有内存屏障的情况下,第一行和最后一行可以在内部优化(粗略地)如下:
1)分配一些内存,在this.utf8NoBomEncoding中存储指向它的指针
2)调用UTF8Encoding构造函数,用有效值填充该内存。
想象一下,如果在步骤1和步骤2之间运行另一个线程并通过此代码。它将使用尚未构建的对象。
答案 1 :(得分:2)
这种模式在.NET中相当常见。这是可能的,因为UTF8Encoding是一个不可变类。是的,可以创建多个类的实例,但这并不重要,因为所有实例都是相同的。使用Equals()覆盖强制执行。额外的副本将很快被垃圾收集。内存屏障只是确保对象状态完全可见。