这种延迟加载缓存实现是否是线程安全的?

我正在使用3.5 .NET Framework进行开发,我需要在multithreadingsenario中使用缓存,并为其项目添加延迟加载模式。 在网上阅读了几篇文章后,我试着编写自己的实现。 这是我的代码。

public class CacheItem { public void ExpensiveLoad() { // some expensive code } } public class Cache { static object SynchObj = new object(); static Dictionary Cache = new Dictionary(); static volatile List CacheKeys = new List(); public CacheItem Get(string key) { List keys = CacheKeys; if (!keys.Contains(key)) { lock (SynchObj) { keys = CacheKeys; if (!keys.Contains(key)) { CacheItem item = new CacheItem(); item.ExpensiveLoad(); Cache.Add(key, item); List newKeys = new List(CacheKeys); newKeys.Add(key); CacheKeys = newKeys; } } } return Cache[key]; } } 

正如您所看到的,Cache对象既使用存储真实键值对的字典,也使用仅复制键的列表。 当一个线程调用Get方法时,它会读取静态共享密钥列表(它被声明为volatile)并调用Contains方法以查看密钥是否已经存在,如果不是,则在开始延迟加载之前使用双重检查的锁定模式。 在加载结束时,创建密钥列表的新实例并将其存储在静态变量中。

显然,我处于这样一种情况,即重新创建整个密钥列表的成本几乎与单个项目加载的成本无关。

我希望有人可以告诉我它是否真的是线程安全的。 当我说“线程安全”时,我的意思是每个读者线程都可以避免读取损坏或脏,并且每个编写器线程只加载缺少的项目一次。

谢谢。

这不是线程安全的,因为在读取Dictionary时没有锁定。

有一个竞争条件,一个线程可以读取:

 return Cache[key]; 

而另一个是写作:

 _Cache.Add(key, item); 

正如Dictionary的MSDN文档所述:`

要允许多个线程访问集合以进行读取和写入,您必须实现自己的同步。

并且您的同步不包括读者。

你真的需要使用一个线程安全的字典,这将极大地简化你的代码(根本不需要List)

我建议获取.NET 4 ConcurrentDictionary的源代码。

正确地获得线程安全是很困难的,因为其他一些回答者错误地声明您的实现是线程安全的事实certificate了这一点。 因此,我相信微软在自制之前的实施。

如果你不想使用线程安全字典,那么我建议一些简单的东西:

 public CacheItem Get(string key) { lock (SynchObj) { CacheItem item; if (!Cache.TryGetValue(key, out item)) { item = new CacheItem(); item.ExpensiveLoad(); Cache.Add(key, item); } return item; } } 

您也可以尝试使用ReaderWriterLockSlim进行实现,但您可能无法获得显着的性能提升(google for ReaderWriterLockSlim性能)。

至于使用ConcurrentDictionary的实现,在大多数情况下我只会使用如下内容:

 static ConcurrentDictionary Cache = new ConcurrentDictionary(StringComparer.Ordinal); ... CacheItem item = Cache.GetOrAdd(key, key => ExpensiveLoad(key)); 

这可能会导致ExpensiveLoad被更多地调用一次,但是我打赌如果你对你的应用进行分析,你会发现这是非常罕见的,不会出现问题。

如果你真的坚持确保只调用一次,那么你可以掌握.NET 4 Lazy实现,并执行以下操作:

 static ConcurrentDictionary> Cache = new ConcurrentDictionary>(StringComparer.Ordinal); ... CacheItem item = Cache.GetOrAdd(key, new Lazy(()=> ExpensiveLoad(key)) ).Value; 

在此版本中,可能会创建多个Lazy实例,但实际上只有一个实例存储在字典中。 ExpensiveLoad将首次调用Lazy.Value被取消引用存储在字典中的实例。 这个Lazy构造函数使用LazyThreadSafetyMode.ExecutionAndPublication,它在内部使用锁,因此确保只有一个线程调用工厂方法ExpensiveLoad

另外,在使用字符串键构造任何字典时,我总是使用IEqualityComparer参数(通常是StringComparer.Ordinal或StringComparer.OrdinalIgnoreCase)来明确记录有关区分大小写的意图。

到目前为止,我看不出任何重大问题。 我在你的代码中唯一看不到的是你如何公开CacheKeys ? 最简单的一个是IList ,它由ReadOnlyCollection填充。 这样,您的消费者可以非常轻松地使用索引运算符或count属性。 在这种情况下,也不需要使用volatile关键字,因为您已将所有内容都放入锁中。 所以我会按如下方式对你的课程进行皮条客:

 public class CacheItem { public void ExpensiveLoad() { // some expensive code } } public class Cache { private static object _SynchObj = new object(); private static Dictionary _Cache = new Dictionary(); private static ReadOnlyCollection _CacheKeysReadOnly = new ReadOnlyCollection(new List()); public IList CacheKeys { get { return _CacheKeysReadOnly; } } public CacheItem Get(string key) { CacheItem item = null; ReadOnlyCollection keys = _CacheKeysReadOnly; if (!keys.Contains(key)) { lock (_SynchObj) { keys = _CacheKeysReadOnly; if (!keys.Contains(key)) { item = new CacheItem(); item.ExpensiveLoad(); _Cache.Add(key, item); List newKeys = new List(_CacheKeysReadOnly); newKeys.Add(key); _CacheKeysReadOnly = newKeys.AsReadOnly(); } } } return item; } } 

作为替代方案,如果您已经在.Net 4.5上,您还可以考虑使用IReadOnlyList接口来实现CacheKeys属性。