【问题标题】:Is this lazy initialization pattern for objects in a hashmap thread-safe?hashmap 中对象的这种惰性初始化模式是线程安全的吗?
【发布时间】:2011-09-08 01:30:56
【问题描述】:

如果可能,我想避免锁定读取。但这“感觉”就像双重检查锁定,即使没有涉及部分初始化的成员。

这是一个好的结构吗?

private final Map<String, Stuff> stash = new HashMap<String, Stuff>();

public Stuff getStuff(String name) {

    if (stash.containsKey(name))
        return stash.get(name);

    synchronized(stash) {
        if (stash.containsKey(name)) {
            return stash.get(name);
        }
        else {
            Stuff stuff = StuffFactory.create(name);
            stash.put(name, stuff);
            return stuff;
        }
    }
}

【问题讨论】:

  • 你应该使用 Guava 的 MapMaker 之类的东西,除非这是为了好玩。

标签: java multithreading concurrency locking hashmap


【解决方案1】:

不,这个结构不是线程安全的。

假设线程writer 正在将一些东西放入地图,而地图太小,必须调整大小。这是在synchronized 块内完成的,所以你可能认为你很好。

在调整大小期间,地图中的任何内容都无法保证。

现在,同时假设线程reader 为现有元素调用getStuff。该线程可以直接访问地图,因为它不会在第一次调用containsKeyget 时遇到synchronized 块。它将找到处于未定义状态的映射,尽管它只读取,但它访问内容未定义的数据。可能的结果包括:

  • getStuff 在不应该的时候返回 null
  • getStuff 返回预期的Stuff
  • getStuff 返回一些内部对象,由 HashMap 实现在调整大小期间使用。
  • getStuff 返回一些其他的 Stuff,与名称无关。
  • getStuff 陷入无限循环。

这只是一个显而易见的例子,应该很容易理解。所以不,当有像 ConcurrentHashMap 或 Guava 的 MapMaker 这样精心设计的类时,不要走捷径。

顺便说一句:用同一个键先调用containsKey,然后调用get,效率相当低。只需调用get,保存结果并将其与null 进行比较。您将在地图中保存一个搜索操作。

【讨论】:

    【解决方案2】:

    HashMap 替换为ConcurrentHashMap,你的实现没问题。

    一个更通用的解决方案不得不担心以下事情

    1 .基于名称的锁定,而不是全局锁定。如果 create(n1) 阻塞,它不应该影响对其他名称的操作。

    2 。如果create() 返回 null 或抛出异常怎么办。有趣的是,这对某些 impl 来说是个问题。

    3 .如果create(n1) 调用get(n1) 会怎样?我们将进行递归。一些 impl 挂起。一些 impl 检测到递归并抛出错误。更好的 impl 应该允许递归运行(终止或堆栈溢出),并且中间结果应该对锁定线程可见,但对其他线程不可见,直到递归终止。

    【讨论】:

      猜你喜欢
      • 2010-11-08
      • 2021-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多