java - 以线程安全的方式延迟初始化 Java 映射

标签 java multithreading dictionary double-checked-locking

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

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 调用发生时数据只加载到 map 中一次。

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

最佳答案

双重检查锁定

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

首先,您需要将 someMap 变成一个 volatile 变量。这是为了让其他线程在更改完成时看到对其所做的更改。

private volatile Map<String, String> someMap = null;

您还需要在 synchronized block 中再次检查 null 以确保在您等待进入 synchronized 时另一个线程没有为您初始化它地区。

    if (someMap == null) {
        synchronized(this) {
            if (someMap == null) {

在准备好使用之前不要分配

在您生成的 map 中,在临时变量中构造它,然后在最后分配它。

                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); 

解释为什么需要临时 map 。一旦完成 someMap = new HashMap... 行,someMap 就不再为空。这意味着对 get 的其他调用将看到它并且永远不会尝试进入 synchronized block 。然后他们将尝试从 map 中获取数据,而无需等待数据库调用完成。

通过确保对 someMap 的赋值是同步块(synchronized block)中防止这种情况发生的最后一步。

不可修改的 map

正如评论中所讨论的,为了安全起见,最好将结果保存在 unmodifiableMap 中,因为 future 的修改将不是线程安全的。对于永远不会公开的私有(private)变量,这并不是严格要求的,但它对 future 来说仍然更安全,因为它会阻止人们稍后进入并在没有意识到的情况下更改代码。

            someMap = Collections.unmodifiableMap(tmpMap);

为什么不使用 ConcurrentMap?

ConcurrentMap 使单个操作(即 putIfAbsent)成为线程安全的,但它不满足此处的基本要求,即在允许读取之前等待 map 完全填充数据从它。

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

为什么要在这个上同步?

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

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

实际上,在许多情况下,单独的锁对象是多余的。这是一个基于你的类的复杂性和使用范围的判断调用,而不是仅仅锁定 this 的简单性。如果有疑问,您应该使用内部锁对象并采取最安全的方式。

在类里面:

private final Object lock = new Object();

在方法中:

synchronized(lock) {

至于java.util.concurrent.locks 对象,它们在这种情况下没有添加任何有用的东西(尽管在其他情况下它们非常有用)。我们总是希望等到数据可用,以便标准同步块(synchronized block)为我们提供所需的行为。

关于java - 以线程安全的方式延迟初始化 Java 映射,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23807601/

相关文章:

ios - 使用 reduce 填充 [String : [CGFloat]] dictionary from an object array

java - 将具有多次重复的 SQL 查询映射到 hibernate 实体

java - 我的可绘制对象会干扰我的按钮状态吗?

java - SQL 语法错误 检查与您的 MariaDB 服务器对应的手册?

c++ - 如何实现线程安全的LRU缓存驱逐?

python - Python字典中的最小值

python - .get 和字典

java - 高级通配符操作从 URL 中删除逗号?

java - 局部变量和线程安全

c# - WCF 仅在满足特定条件时调用方法,否则等待