懒惰地以线程安全的方式初始化Java映射

时间:2014-05-22 13:01:39

标签: java multithreading dictionary double-checked-locking

我需要懒洋洋地初始化地图及其内容。我现在有以下代码:

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为空时出现,继续将字段初始化为new HashMap并且当它仍然在地图中加载数据时,另一个线程确实一个getValue并且当一个人可能存在时,它不会得到数据。

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

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

4 个答案:

答案 0 :(得分:25)

双重检查锁定

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

首先,您需要将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不再为空。这意味着对get的其他调用将会看到它,并且永远不会尝试输入synchronized块。然后,他们将尝试从地图获取而无需等待数据库调用完成。

确保someMap的分配是同步块中阻止这种情况发生的最后一步。

<强> unmodifiableMap

正如评论中所讨论的那样,为了安全起见,最好将结果保存在unmodifiableMap中,因为将来的修改不会是线程安全的。对于从未公开过的私有变量,这并不是严格要求的,但它对未来仍然更安全,因为它会阻止人们稍后进入并更改代码而不会意识到。

            someMap = Collections.unmodifiableMap(tmpMap);

为什么不使用ConcurrentMap?

ConcurrentMap使个别操作(即putIfAbsent)线程安全,但它不符合此处的基本要求,即在允许从中读取数据之前等待地图完全填充数据。

此外,在这种情况下,延迟初始化后的Map不再被修改。 ConcurrentMap会为在此特定用例中不需要同步的操作添加同步开销。

为什么要同步

没有理由。 :)这只是提出这个问题的有效答案的最简单方法。

在私有内部对象上进行同步肯定会更好。您已经改进了封装,因为内存使用量和对象创建时间略有增加。在this上同步的主要风险是它允许其他程序员访问您的锁对象并可能尝试自己同步。这会导致他们的更新与您的更新之间发生不必要的争用,因此内部锁定对象更安全。

实际上,虽然在许多情况下单独的锁定对象是矫枉过正的。它是基于您的类的复杂性的判断调用,以及对this的锁定的简单性使用的广泛程度。如果有疑问,您应该使用内部锁定对象并采取最安全的路线。

在课堂上:

private final Object lock = new Object();

在方法中:

synchronized(lock) {

对于java.util.concurrent.locks个对象,他们不会在这种情况下添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,这样标准的同步块就能完全满足我们所需的行为。

答案 1 :(得分: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
    }
}

答案 2 :(得分:1)

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

  1. 每个值的生成/存储同样昂贵
  2. 值的生成是昂贵的,但是如果你生成一个,生成其余的就不再那么昂贵了(例如你需要查询数据库)
  3. Guava library有两种解决方案。使用Cache动态生成值或CacheLoader + loadAll批量生成值。由于空Cache的初始化几乎是免费的,因此无需使用double check idiom:只需将Cache实例分配给private final字段。

答案 3 :(得分:-6)

您可以使用以下代码获取具有类级别锁定的同步映射。

映射aSynchronizedMap = Collections.synchronizedMap(new HashMap());

class SomeClass {
    private Map<String, String> someMap = null;

    public String getValue(String key) {
        if (someMap == null) {
            synchronized (SomeClass.class) {

                  someMap  = Collections.synchronizedMap(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
    }
}