我想尽可能避免锁定读取。但这种“感觉”就像双重检查锁定一样,即使没有涉及部分初始化的成员。
这是一个好的构造吗?
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;
}
}
}
答案 0 :(得分:6)
不,这个构造不是线程安全的。
假设线程writer
正在将某些内容放入地图中,并且地图太小,必须调整大小。这是在synchronized
区块内完成的,所以你可能认为你很好。
在调整大小期间,地图中没有任何内容可以保证。
现在,在同一时间,假设线程reader
为现有元素调用getStuff
。此主题可以直接访问地图,因为第一次调用synchronized
和containsKey
时,它没有点击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应该允许递归运行(终止或堆栈溢出),并且中间结果应该对锁定线程可见,但对其他线程不可见,直到递归终止。