ConcurrentHashMap
的JavaDoc是这样说的:
与
类似,但与Hashtable
不同,此类不允许允许将HashMap
用作键或值。null
我的问题:为什么?
第二个问题:为什么
Hashtable
不允许为空?
我使用了很多 HashMap 来存储数据。但是当更改为
ConcurrentHashMap
时,我因为NullPointerExceptions而陷入了几次麻烦。
ConcurrentHashMap
的作者本人(Doug Lea):
ConcurrentMaps 中不允许使用 null 的主要原因 (ConcurrentHashMaps, ConcurrentSkipListMaps) 的歧义在于 在非并发地图中可能只是勉强可以忍受不能 容纳。主要的一点是,如果
返回map.get(key)
,那么你 无法检测该键是否显式映射到null
与该键不是 映射。在非并发地图中,您可以通过以下方式检查null
,但在并发的情况下,地图可能已经改变 通话之间。map.contains(key)
我相信,至少部分地,允许您将
containsKey
和 get
合并到一个调用中。如果映射可以保存 null,则无法判断 get
是否返回 null,因为没有该值的键,或者只是因为该值为 null。
为什么这是一个问题?因为没有安全的方法可以让你自己做到这一点。采取以下代码:
if (m.containsKey(k)) {
return m.get(k);
} else {
throw new KeyNotPresentException();
}
由于
m
是并发映射,因此可能会在 containsKey
和 get
调用之间删除键 k,导致此代码段返回表中从未存在的 null,而不是所需的 KeyNotPresentException
。
通常您可以通过同步来解决这个问题,但是使用并发映射当然是行不通的。因此
get
的签名必须更改,而以向后兼容的方式做到这一点的唯一方法是防止用户首先插入空值,并继续使用它作为“找不到密钥”的占位符.
乔什·布洛赫设计了
HashMap
;道格·李 (Doug Lea) 设计了ConcurrentHashMap
。我希望这不是诽谤。实际上,我认为问题在于空值通常需要包装,以便真正的空值可以代表未初始化。如果客户端代码需要空值,那么它可以支付包装空值本身的(诚然很小)成本。
您无法在 null 上同步。
编辑:在这种情况下,这并不是真正的原因。我最初认为锁定事物以防止并发更新或以其他方式使用对象监视器来检测某些内容是否被修改是很奇特的事情,但在检查源代码后,看来我错了 - 他们使用基于“段”的锁定在哈希的位掩码上。
在这种情况下,我怀疑他们这样做是为了复制 Hashtable,我怀疑 Hashtable 这样做是因为在关系数据库世界中,null != null,所以使用 null 作为键没有任何意义。
我猜下面的 API 文档片段给出了很好的提示: “在依赖其线程安全性但不依赖其同步细节的程序中,此类可与 Hashtable 完全互操作。”
他们可能只是想让
ConcurrentHashMap
与 Hashtable
完全兼容/可互换。并且由于 Hashtable
不允许空键和值..
ConcurrentMaps(ConcurrentHashMaps、ConcurrentSkipListMaps)中不允许使用 null 的主要原因是无法容纳在非并发映射中勉强可以容忍的歧义。主要的一点是,如果map.get(key)返回null,你无法检测该键是否显式映射到null与该键是否未映射。在非并发映射中,您可以通过 map.contains(key) 检查这一点,但在并发映射中,映射可能在调用之间发生了变化。
对于空键,我认为当散列碰撞时需要使用第一个节点作为锁定对象,就像
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
如果 f 为 null ,则会抛出 nullpointException 同步(f){ }
ConcurrentHashMap 是线程安全的。我相信不允许空键和值是确保线程安全的一部分。
我认为禁止空值不是一个正确的选择。 在很多情况下,我们确实希望将一个具有空值的键放入并发映射中。但是,通过使用 ConcurrentHashMap,我们无法做到这一点。 我建议即将推出的 JDK 版本可以支持这一点。