散列映射中对象的这种延迟初始化模式是否是线程安全的?

时间:2011-09-08 01:30:56

标签: java multithreading concurrency locking hashmap

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

这是一个好的构造吗?

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

2 个答案:

答案 0 :(得分:6)

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

假设线程writer正在将某些内容放入地图中,并且地图太小,必须调整大小。这是在synchronized区块内完成的,所以你可能认为你很好。

在调整大小期间,地图中没有任何内容可以保证。

现在,在同一时间,假设线程reader为现有元素调用getStuff。此主题可以直接访问地图,因为第一次调用synchronizedcontainsKey时,它没有点击get块。它将找到未定义状态的映射,虽然它只读取,但它访问内容未定义的数据。可能的结果包括:

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

这只是一个应该易于理解的明显案例。所以不,当有精心设计的类ConcurrentHashMap或番石榴MapMaker时,请不要使用快捷方式。

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

答案 1 :(得分:1)

HashMap替换为ConcurrentHashMap,您的impl很好。

更通用的解决方案必须担心以下事情

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

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

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