7中本质上还是采用链表+数组的形式存储键值对的。但是,为了提高并发,把原来的整个 table
划分为 n
个 Segment
。所以,从整体来看,它是一个由 Segment
组成的数组。然后,每个 Segment
里边是由 HashEntry
组成的数组,每个 HashEntry
之间又可以形成链表。我们可以把每个 Segment
看成是一个小的 HashMap
,其内部结构和 HashMap
是一模一样的。
当对某个 Segment
加锁时,并不会影响到其他 Segment
的读写。每个 Segment
内部自己操作自己的数据。这样一来,我们要做的就是尽可能的让元素均匀的分布在不同的 Segment
中。最理想的状态是,所有执行的线程操作的元素都是不同的 Segment
,这样就可以降低锁的竞争。
先看put方法的源码
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
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;
if (tab == null || (n = tab.length) == 0)
// 首次put的时候会先去初始化hash桶
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果该位置还为null,说明该位置上还没有记录,
// 则通过调用casTabAt方法来讲该新的记录插入到table的index位置上去
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 通过synchronized关键字对table的index位置加锁
// 当前线程只会锁住table的index位置,其他位置上没有锁住
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 判断table的index位置上的第一个节点的hashCode值,大于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)))) {
// 寻找是否已经有记录的key和当前想要插入的记录是一致的,如果一致,
// 那么这次put的效果就是replace
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, null);
break;
}
}
}
else if (f instanceof TreeBin) {
// 判断table的index位置上的第一个节点的hashCode值,小于0 说明是一个红黑树
Node<K,V> p;
binCount = 2;
// 调用putTreeVal方法来进行插入操作
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
可以看出put方法的逻辑是这样的
- 计算记录的
key
的hashCode
- 计算
table
的index
位置,如果该位置还为null
,说明该位置上还没有记录 - 通过调用
casTabAt
方法来将该新的记录插入到table
的index
位置上去 - 如果该位置不为
null
,通过synchronized
关键字对table
的index
位置加锁,然后判断table
的index
位置上的第一个节点的hashCode
值,这个值大于零则说明此处是一个链表,小于0说明是一个红黑树 - 执行链表或者红黑树的插入节点方法
看起来JDK8中的ConcurrentHashMap
结构和HashMap
结构几乎是一样的,区别就在于插入的时候会对节点进行加锁。相对于 1.7 来说,锁的粒度降低了,效率也提高了。