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