1. 介绍
在并发编程中使用HashMap可能导致程序死循环或者数据丢失,而使用线程安全的HashTable效率又非常低下,基于上述两个原因,才有ConcurrentHashMap。
1.1 JDK1.7的ConcurrentHashMap
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每 个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先 获得对应的 Segment的锁。
-
该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
-
Segment是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁;
1.2 JDK1.8的ConcurrentHashMap
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:
链表或红黑二叉树首节点用CAS插入,Synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发。
2. 源码
2.1 JDK1.8的ConcurrentHashMap
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//两次hash,减少hash冲突,可以均匀分布
int hash = spread(key.hashCode());
int binCount = 0;
//对这个table进行迭代
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
//这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//如果i位置没有数据,就直接无锁插入
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 (f) {
//如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
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)))) {
//这里涉及到相同的key进行put就会覆盖原先的value
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) {
//红黑树结构
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
//如果链表的长度大于8时就会进行红黑树的转换
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//统计size,并且检查是否需要扩容
addCount(1L, binCount);
return null;
}
3. 实践
4. FAQ
5.1 JAVA8的ConcurrentHashMap为什么放弃了分段锁
-
加入多个分段锁浪费内存空间。
-
生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
-
为了提高 GC 的效率(为什么呢)
-
个人而言JDK1.8 HashMap采用的是尾插法,而JDK1.7 采用的是头插法,头插法首节点不固定,无法用Synchronized去锁住对象。一旦用到锁,则尽可能缩小锁的粒度。
5.2 ConcurrentHashMap和Hashtable的区别?
ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。
5.3 CAS是非阻塞算法