JDK版本1.8
|
数据结构
|
底层实现
|
初始容量
|
扩容
|
负载因子(默认值)
|
线程安全
|
链表插入值方法
|
|
ConcurrentHashMap
|
Node数组+链表/红黑树
|
16
|
采用多线程扩容,整个扩容过程,通过CAS设置sizeCtl,transferIndex等变量协调多个线程进行并发扩容
|
0.75
|
是(采用CAS + synchronized
保证并发安全)
|
尾插法
|
ConcurrentHashMap
1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。
那就是查询遍历链表效率太低
因此 1.8 做了一些数据结构上的调整,首先来看下底层的组成结构:
- key和value都不能为null
- 其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性
- 链表的对象是用Node对象
- 红黑树是使用TreeBin对象,TreeBin对象中包含TreeNode对象属性,使用TreeBin对象包裹TreeNode对象是为了保证红黑树在操作的时候将锁加载TreeBin对象上保证锁的一致性,因为如果将锁加在TreeNode对象上可能会出现红黑树操作时根节点发生变化,导致旧的根节点上的锁变成了子节点锁,这样就失去了红黑树锁的作用
- 初始化initTable时使用synchronized锁保证多线程并发安全
- ConcurrentHashMap的数量计数方式是通过basecount属性来计算个数,不过在多线程并发的时候会通过CounterCell数组来将计数的basecount属性的锁的竞争分散开,例如一个线程竞争到了basecount的锁,这个时候其他线程就不会在去竞争basecount的锁,而是去生成一个随机数和CounterCell数组的length-1进行&操作得到一个CounterCell的数组下标,将CounterCell里的Value值+1,如图:
源码解析
1.初识ConcurrentHashMap实现
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
Node(int hash, K key, V val, Node next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node find(int h, Object k) {
Node e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
- 其中将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的
- 其中的 val next 都用了 volatile 修饰,保证了可见性
2.put方法
2-1 put方法一览
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[] tab = table;;) { //根据 key 计算出 hashcode
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) //判断是否需要进行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) //如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { //如果都不满足,则利用 synchronized 锁写入数据
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node 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 pred = e;
if ((e = e.next) == null) {
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put方法大致过程如下:
- 根据 key 计算出 hashcode
- 判断是否需要进行初始化
- f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
- 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
- 如果都不满足,则利用 synchronized 锁写入数据
- 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树
2.get方法
2-1 get方法一览
public V get(Object key) {
Node[] tab; Node e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get方法大致过程如下:
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值
- 如果是红黑树那就按照树的方式获取值
- 如果不满足红黑树那就按照链表的方式遍历获取值
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。