双重检查锁定
双重检查锁定需要几个步骤才能完成才能正常工作,您缺少其中两个步骤。
首先,您需要制作一个变量。这样,其他线程在进行更改时(但在更改完成后)将看到对它所做的更改。someMapvolatile
private volatile Map<String, String> someMap = null;
您还需要对块内部进行第二次检查,以确保在您等待进入同步区域时,另一个线程尚未为您初始化它。nullsynchronized
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...someMapgetsynchronized
通过确保分配到是同步块中防止这种情况发生的最后一步。someMap
不可修改的地图
正如评论中所讨论的,为了安全起见,最好将结果保存在一个中,因为将来的修改不会是线程安全的。对于从不公开的私有变量来说,这并不是严格要求的,但是对于未来来说,它仍然更安全,因为它可以阻止人们稍后进入并在没有意识到的情况下更改代码。unmodifiableMap
someMap = Collections.unmodifiableMap(tmpMap);
为什么不使用 ConcurrentMap?
ConcurrentMap使单个操作(即)线程安全,但它不符合这里的基本要求,即等到映射完全填充数据后才允许从中读取。putIfAbsent
此外,在这种情况下,惰性初始化后的 Map 不会再次被修改。这将为在此特定用例中不需要同步的操作增加同步开销。ConcurrentMap
为什么要在此上进行同步?
没有理由。:)这只是为这个问题提供有效答案的最简单方法。
在私有内部对象上进行同步肯定是更好的做法。您已经改进了封装,但代价是内存使用量和对象创建时间略有增加。同步的主要风险是它允许其他程序员访问您的锁定对象,并可能尝试自己进行同步。这会导致他们的更新和你的更新之间不必要的争用,因此内部锁定对象更安全。this
实际上,尽管在许多情况下,单独的锁定对象是过度的。这是一个基于类的复杂性以及使用范围与锁定的简单性相比的判断调用。如果有疑问,您应该使用内部锁定对象并采取最安全的途径。this
在课堂上:
private final Object lock = new Object();
在方法中:
synchronized(lock) {
至于对象,在这种情况下,它们不会添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,因此标准的同步块为我们提供了所需的行为。java.util.concurrent.locks