16

我需要延迟初始化地图及其内容。到目前为止,我有以下代码:

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,当它仍在地图中加载数据时,另一个线程getValue在可能存在的情况下执行并且不获取数据.

如何确保在第一次getValue调用发生时数据仅在地图中加载一次。

请注意,key在所有初始化之后,地图中可能不存在 。此外,在所有初始化之后,地图可能只是空的。

4

3 回答 3

27

双重检查锁定

双重检查锁定需要完成几个步骤才能正常工作,您缺少其中两个。

首先,您需要制作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对象,在这种情况下它们不会添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,以便标准同步块为我们提供所需的行为。

于 2014-05-22T13:06:49.343 回答
2

我认为 TimB 很好地解释了大多数选项,但我认为最快和最明显的答案是在实例化类实例时创建它。

class SomeClass {
    private final Map<String, String> someMap = new HashMap<String, String>();

    public String getValue(String key) {
        return someMap.get(key);  // the key might not exist even after initialization
    }
}
于 2014-05-22T15:34:53.760 回答
1

您想要延迟初始化地图的原因是因为值的生成是资源密集型的。通常,您可以区分两个用例

  1. 每个值的生成/存储同样昂贵
  2. 生成值很昂贵,但如果您生成一个,生成其余的不再那么昂贵(例如,您需要查询数据库)

Guava 库为这两者提供了解决方案。使用Cache动态生成值或使用CacheLoader +loadAll批量生成值。由于空 Cache 的初始化实际上是免费的,因此无需使用双重检查习惯用法:只需将 Cache 实例分配给一个private final字段。

于 2014-05-22T13:04:46.507 回答