双重检查锁定
双重检查锁定需要完成几个步骤才能正常工作,您缺少其中两个。
首先,您需要制作someMap
一个volatile
变量。这是为了让其他线程在更改完成后才能看到对其所做的更改。
private volatile Map<String, String> someMap = null;
null
您还需要对块内部进行第二次检查,synchronized
以确保在您等待进入同步区域时另一个线程没有为您初始化它。
if (someMap == null) {
synchronized(this) {
if (someMap == null) {
在准备好使用之前不要分配
在您生成地图时,将其构造在一个临时变量中,然后在最后分配它。
Map<String, String> tmpMap = new HashMap<String, String>();
// initialize the map contents by loading some data from the database.
// possible for the map to be empty after this.
someMap = tmpMap;
}
}
}
return someMap.get(key);
解释为什么需要临时地图。一旦您完成该行someMap = new HashMap...
,则someMap
不再为空。这意味着其他调用get
将看到它并且永远不会尝试进入该synchronized
块。然后,他们将尝试从地图中获取,而无需等待数据库调用完成。
通过确保分配到someMap
是同步块中防止这种情况发生的最后一步。
不可修改的地图
正如评论中所讨论的,为了安全起见,最好将结果保存在 an 中,unmodifiableMap
因为将来的修改将不是线程安全的。对于从不公开的私有变量来说,这并不是严格要求的,但对于未来来说它仍然更安全,因为它可以阻止人们稍后进入并在没有意识到的情况下更改代码。
someMap = Collections.unmodifiableMap(tmpMap);
为什么不使用 ConcurrentMap?
ConcurrentMap
使单个操作(即putIfAbsent
)线程安全,但它不满足此处的基本要求,即等待地图完全填充数据后再允许读取。
此外,在这种情况下,延迟初始化后的 Map 不会再次被修改。这ConcurrentMap
会给在这个特定用例中不需要同步的操作增加同步开销。
为什么要同步这个?
没有理由。:) 这只是对这个问题提出有效答案的最简单方法。
在私有内部对象上同步肯定是更好的做法。您已经改进了封装,以略微增加内存使用和对象创建时间。同步的主要风险this
是它允许其他程序员访问您的锁定对象并可能尝试自己同步它。这会导致它们的更新与您的更新之间发生不必要的争用,因此内部锁对象更安全。
实际上,尽管在许多情况下单独的锁定对象是多余的。这是一个基于你的类的复杂性的判断调用,以及使用的广泛程度与仅仅锁定的简单性相比this
。如果有疑问,您可能应该使用内部锁定对象并采取最安全的路线。
在课堂里:
private final Object lock = new Object();
在方法中:
synchronized(lock) {
至于java.util.concurrent.locks
对象,在这种情况下它们不会添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,以便标准同步块为我们提供所需的行为。