我已经看过几次了,但我不确定它是否真的不正确。
考虑以下示例类:
class Foo
{
List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
{
var newList = new List<string>(items);
lock (lockedList)
{
lockedList = newList;
}
}
public void Add(string newItem)
{
lock (lockedList)
{
lockedList.Add(newItem);
}
}
public void Contains(string item)
{
lock (lockedList)
{
lockedList.Contains(item);
}
}
}
ReplaceList 覆盖 lockedList,同时它有一个锁。一旦它完成,所有后续调用者实际上将锁定新值。他们可以在 ReplaceList 退出锁定之前进入他们的锁定。
尽管通过替换锁定对象来提高标志,但这段代码实际上可能会正常工作。只要赋值是锁的最后一条语句,就不会再运行同步代码。
除了确保分配保留在锁 block 末尾会增加维护成本之外,还有其他原因可以避免这种情况吗?
请您参考如下方法:
因此,首先,由于您访问该字段的方式的特殊性,您提供的特定解决方案并不安全。
获得可行的解决方案非常容易。只是不要创建新列表;相反,清除它并添加新项目:
class Foo
{
private List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
{
lock (lockedList)
{
lockedList.Clear();
lockedList.AddRange(items);
}
}
public void Add(string newItem)
{
lock (lockedList)
{
lockedList.Add(newItem);
}
}
public void Contains(string item)
{
lock (lockedList)
{
lockedList.Contains(item);
}
}
}
现在您的领域实际上并没有发生变化,您无需担心可能导致的所有问题。
至于问题中的代码如何破解,只需调用 Add 或 Contains 即可读取字段、获取列表、锁定列表,然后让另一个线程替换该字段。当您第二次读取该字段时,在已经获得要锁定的值之后,该值可能已经更改,因此您最终会改变或从列表中读取另一个调用者不会被限制访问的列表。
综上所述,改变 lockedList 变量是一个非常的坏主意,毫无疑问,您应该避免改变它,如上所示,您还可以确保您实际上只读取一次该字段,而不是重复读取它,并且您仍将确保每个列表在任何时候都只能从单个线程访问:
class Foo
{
private volatile List<string> lockedList = new List<string>();
public void ReplaceList(IEnumerable<string> items)
{
lockedList = new List<string>(items);
}
public void Add(string newItem)
{
var localList = lockedList;
lock (localList)
{
localList.Add(newItem);
}
}
public void Contains(string item)
{
var localList = lockedList;
lock (localList)
{
localList.Contains(item);
}
}
}
请注意,这里修复的问题不是改变要锁定的对象从中获取的字段(这不是固有的问题,尽管这是一个非常糟糕的做法),而是在 lock 语句的内部在它的所有用法中不断从字段中获取新值,并期望该值永远不会改变,如果可以的话。
这将更难维护,非常脆弱,并且更难以理解或确保其正确性,所以再一次,做这样的事情。
