一、自我提问
二、整体概括
- 代码量:JDK1.7中ConcurrentHashMap代码2000行,现在JDK1.8中代码多达6000行,可见其复杂度
- 数据结构:数组+链表+红黑树
- 性能:ConcurrentHashMap中数据结构是数组+链表+红黑树基本和HashMap的数据结构一样,log红黑树的时间复杂度是O(logn),数组是O(n)
- 线程安全:ConcurrentHashMap最大的特点是线程安全,一个线程进行put/remove操作时,对桶(链表 or 红黑树)加上synchronized独占锁;
- 锁分离:一个线程每次对一个桶(链表 or 红黑树)进行加锁,其他线程仍然可以访问其他桶,老版本的是对多个hash桶抽象出一个segment的模型,一个segment包含多个hash桶,并发的时候针对segment上锁,一个segment一把锁,这样锁的粒度比较粗,并发的效率没有针对单个Hash桶上锁的效率高
三、核心设计
ConcurrentHashMap的核心设计点包括:数据结构、hash函数设计、hash函数冲突解决机制、容量控制、并发处理、性能设计、功能设计等;
1.hash方法设计
- hash函数首先通过key的自带hashCode()方法获取hash值,自带的hashCode()也就是每个Java对象的父类Object.hashCode()方法,Object.hashCode()方法是根据本地方法实现的,根据查阅资料显示,它其实是通过随机数实现的
- 根据Object.hashCode()生成的hash值有一个缺点是分布不均衡,hash值的二进制中比特位1很容易集中在高位,于是官网将hash值的二进制中比特位高位像右移动了16位,然后将高位变为0,官方解释这是为了降低高位bit位1集中后对hash值分散性的影响,同时这也是性能,系统损耗,hash分散性的综合考虑
- 如果你仍然怀疑这样hash冲突比较多,你可以重写key的hashCode()和eques()方法来提高key的hash分散性
- 其实再简单的理解官方就是通过 XORs 来提高hash值得分散性,因为 XORs 是原生指令效率比较高
下面是hash函数的总结翻译:
/**
* Spreads (XORs) higher bits of hash to lower and also forces top
* bit to 0.
*
* 通过异或将hash值中的高位转换到低位,将高位的比特位转到低位,强制将高位的比特位转为0
*
* Because the table uses power-of-two masking, sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward.
* 因为table的大小是2的幂,hash值容易碰撞在高位,特别是当key的集合是一个浮点数的时候,
* 更加容易产生冲突
* 因此我们将hash值得高位比特位转移到低位,让比特位的1分散更加均匀,降低高位比特位的影响
*
* There is a tradeoff between speed, utility, and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading), and because we use trees to handle large sets of
* collisions in bins, we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage, as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*
* 这种做法是一个通过以速度和性能来换取bit分布散性更好的做法,是一个权衡的结果;
* 因为常见的集合的hash已经分布非常均衡了,这些常见的集合不会受益于这种设计;
* 也就是大多数情况下,集合的hash分布很均衡,这样的设计对它意义不大,
* 同时在hash桶中,我们使用树来处理大量的碰撞;
* 我们仅仅采取了异或bit位这种方式来降低系统性能损耗,这种方式同样也降低了bit位,
* 高位对分散性的影响,要不是由于表的限制;
*
* (h ^ (h >>> 16)) & HASH_BITS 详细解析:
*
* HashMap的值:h = key.hashCode()) ^ (h >>> 16
* 比HashMap多并上了一个HASH_BITS;
*
* (h >>> 16):将高比特位转移到低位
* (h ^ (h >>> 16)):将高比特位变成 0
* (h ^ (h >>> 16)) & HASH_BITS:HASH_BITS是0x7fffffff,该步是为了消除最高位上的负符号
* hash的负在ConcurrentHashMap中有特殊意义表示在扩容或者是树节点
*/
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
2.解释一下put方法的流程
- 通过
int hash = spread(key.hashCode())
计算出hash值; - 在添加数据之前还需要判断table数据是不是为空,如果是空需要先初始化容量,这算是一个设计点,通过懒加载提高空间利用率,在第一个数据添加到Map的时候才初始化空间;
- 两层遍历循环,第一层对table数组做遍历,判断新增数据是不是hash桶的第一个元素,如果是直接添加;如果新增的元素不是hash桶的第一个,需要第二次遍历,这次是对hash桶遍历(此时的hash桶有可能是链表数据结构,有可能是红黑树的结构),查找是不是有相同元素存在了,如果存在默认是覆盖,如果不存在就在hash桶中插入新数据;
- 插入新数据的时候判断,如果插入新数据后hash桶的容量触发 链表转红黑树的阈值 8,则需要将hash桶的链表数据结构转换成红黑树;
- 插入新数据完成后判断,如果map的容量是不是已经触发扩容的加载因子,如果是,则做扩容操作;
- 到目前还没介绍到并发的情况:
- 在初始化map的时候会通过CAS操作判断其它线程是不是正在初始化;
- 在每次插入数据的时候,如果发现发现当前插入数据的hash桶第一个节点已经存在了,那么将会对hash桶上一把synchronized (f)锁,f 表示hash桶的第一个节点,所以hash桶的数量就是ConcurrentHashMap的最大并发数量;
- 在插入数据如果正遇到有其它线程正在扩容,当前线程或加入到库容的任务中,帮助正在扩容的线程移动数据,共同加油
3.解释一下get方法
get方法执行流程
首先定位到具体的hash槽,若hash槽不为空,判断第一个结点是否是要查找的结点(判断方法是先比较hash值,若相等则需要地址相等或者equals为true中的一个成立,则是要查找的结点),否则根据hash值是否为负数,将查找操作分派给相应的find函数。若是ForwardingNode,则用find函数转发到nextTable上查找;若是TreeBin结点,调用TreeBin的find函数,根据自身读写锁情况,去红黑树中查找。最后如果是普通结点,则遍历链表来寻找。
从代码上也可以看出,get操作是无锁的 。后文中即使TreeBin的find函数虽然有可能会加TreeBin的内部读锁,但也是非阻塞的。
- spread计算hash值;
- 判断table不为空;
- 定位tabAt(i)所处hash桶位不为空;
- 检查hash桶第一个元素是不是满,是则返回当前Node的value;否则分别根据树、链表查询;
get方法怎么每次都读取的最新值
- 从源码中可以看出来,在get操作中,根本没有使用同步机制,也没有使用unsafe方法,所以读操作是支持并发操作的;
- 那为什么是安全的并发读呢?2点原因:
- 如果get和put是在hash桶的第一个节点冲突,这时候在get和put方法中都使用了CAS操作保证并发安全
- 如果get和put是在hash桶的非第一个节点冲突,这时候在put方法中使用了synchronized锁住第一个节点,来保证整个hash桶的线程安全
4.“帮助邻居”的思想
解释一下重新扩容的时候,如果多线程并发扩容是怎么实现的,比如“帮助邻居”的思想“。
ConcurrentHashMap无锁多线程扩容,减少扩容时的时间消耗。
参考链接3
transfer扩容操作:单线程构建两倍容量的nextTable;允许多线程复制原table元素到nextTable。
为每个内核均分任务,并保证其不小于16;
若nextTab为null,则初始化其为原table的2倍;
死循环遍历,直到finishing。
节点为空,则插入ForwardingNode;
链表节点(fh>=0),分别插入nextTable的i和i+n的位置;
TreeBin节点(fh<0),判断是否需要untreefi,分别插入nextTable的i和i+n的位置;
finishing时,nextTab赋给table,更新sizeCtl为新容量的0.75倍 ,完成扩容
5.怎么计算的容量大小
每个hash桶有一个计数器:CounterCell,有多少个hash桶就有多少个计数器,设计了一个计数器的数;
CounterCell对象里面封装了一个 volatile long value 变量;
每次求整个map的容量的时候,循环计数器数组,累加完成;
6.红黑树和链表互相转
红黑树将链表的查询时间效率从O(n) 提升到了log(n)
7.从map中移除一个元素
删除一个key就是将这个key的value设置为null
删除的逻辑和put有点相似
- 检查hash桶的第一个节点,是否满足删除要求
- 如果不满足则对hash桶的第一个节点上锁,然后根据hash桶的数据结构链表或者树,采取不同的查询方法,遍历hash桶,找到满足要求的节点,删除节点。
/**
* Removes the key (and its corresponding value) from this map.
* This method does nothing if the key is not in the map.
*
* @param key the key that needs to be removed
* @return the previous value associated with {@code key}, or
* {@code null} if there was no mapping for {@code key}
* @throws NullPointerException if the specified key is null
*/
public V remove(Object key) {
return replaceNode(key, null, null);
}
/**
* Implementation for the four public remove/replace methods:
* Replaces node value with v, conditional upon match of cv if
* non-null. If resulting value is null, delete.
*/
final V replaceNode(Object key, V value, Object cv) {
int hash = spread(key.hashCode());
//第一层循环,循环table
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null){
// 如果要删除的节点为null,直接退出,hash桶的第一个节点为空
break;
} else if ((fh = f.hash) == MOVED){
// 如果删除数据的时候碰到正在扩容,那么删除线程加入并发扩容的阵营中去,‘帮助思想’的体现
tab = helpTransfer(tab, f);
}
else {
V oldVal = null;
boolean validated = false;
//将hash桶上锁,对第一个节点上锁,锁住入口
synchronized (f) {
if (tabAt(tab, i) == f) {
//遍历链表,寻找满足的节点删除
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
// 如果发现当前hash桶已经是一棵树了,通过遍历树,删除满足要求的节点
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
- remove(Object key)
- addCount(long x, int check)
- helpTransfer(Node<K,V>[] tab, Node<K,V> f)
- resizeStamp(int n)
- tryPresize(int size)
- transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
- fullAddCount(long x, boolean wasUncontended)
- treeifyBin()
- untreeify(Node<K,V> b)
- size()
四、参考连接
- 【JUC】JDK1.8源码分析之ConcurrentHashMap(一)
- 【JUC】JDK1.8源码分析之ConcurrentSkipListMap(二)
- ConcurrentHashMap源码分析–Java8
- 为什么ConcurrentHashMap是弱一致的
五、源码翻译
ConcurrentHashMap阅读笔记.java