一、HashMap源码解读
1、put操作
-
对key的hashCode进行hash,然后再计算index;
-
如果没有碰撞直接放到桶bucket里
-
如果碰撞里了,以链表的形式存在bucket里
-
如果碰撞导致链表长度过长(大于等于TREEIFY_THRESHOLD),就把链表转换为红黑树
-
如果节点已经存在就替换old value(保证key的唯一性)
-
如果bucket满了(超过load factor*current capacity),就要resize.
public V put(K key, V value) {
//对key的hashCode进行hash
return putVal(hash(key), key, value, false, true);
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//hash table对象
Node<K,V>[] tab;
//下标i对应的Node对象
Node<K,V> p;
//key在hash table中存放的索引下标
int n, i;
// 获取hash table 的长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// i = (n - 1) & hash 根据hash值计算key在hash table中的位置,根据这行代码可以得出一个结论:如果key为null(此时对应的hash为0),那么一定是在下标为0的位置
if ((p = tab[i = (n - 1) & hash]) == null)
//如果下标i的位置是null(尚未有元素),那么直接放入
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值是否相等(p的key和输入的key相等)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录
e = p;
// hash值不相等,即key不相等;为红黑树结点
else if (p instanceof TreeNode)
// 此时已经是个红黑树了,继续往红黑树里添加元素
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//该链为链表
else {
// 如果是传统的列表对象(在链表最末插入结点)
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部(此if的目的是找到位置i上的链表/红黑树的最后一个Node元素)
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(TREEIFY_THRESHOLD = 8),转化为红黑树(如果hash table的长度不到MIN_TREEIFY_CAPACITY即64,那么只做扩容处理,并不会转换为红黑树)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
// 跳出循环
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相等,跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中找到key值、hash值与插入元素相等的结点
if (e != null) { // existing mapping for key
// 记录e的value
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 访问后回调
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改
++modCount;
// 实际大小大于阈值则扩容
if (++size > threshold)
resize();
// 插入后回调
afterNodeInsertion(evict);
return null;
}
2、resize的实现
当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。在resize的过程,简单的说就是把bucket扩充为2倍,之后重新计算index,把节点再放到新的bucket中。当超过限制的时候会resize,然而又因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。
final Node<K,V>[] resize() {
//旧数组
Node<K,V>[] oldTab = table;
//旧容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧扩容门槛
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 超过最大值就不再扩充了,就只好随你碰撞去吧
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果旧容量的两倍小于最大容量并且旧容量大于默认初始值(16),则容量扩大为两倍,扩容门槛也扩大为两倍,没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 使用非默认构造方法创建的map,第一次插入元素会走到这里
// 如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//调用默认构造方法创建的map,第一次插入元素会走到这里
//如果旧容量、旧扩容门槛都是0,说明还未初始化过,则初始化容量为默认容量,扩容门槛为默认容量*默认装载因子
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//赋值扩容门槛为新门槛
threshold = newThr;
//新建一个新容量的数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把桶赋值为新数组
table = newTab;
//如果旧数组不为空,则搬移元素
if (oldTab != null) {
//遍历数组, 把每个bucket都移动到新的buckets中
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果桶中的第一个元素不为空,赋值给e
if ((e = oldTab[j]) != null) {
//旧桶赋值为null,便于GC回收
oldTab[j] = null;
//如果这个桶中只有一个元素,则计算他在新桶中的位置并把它搬移到新桶中
//因为每次都扩容为两倍,所以这里的第一个元素搬移到新桶的时候肯定还没有元素
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果第一个元素是树节点,则把这棵树打散成两颗树插入到新桶中
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果这个链表不止一个元素且不是一颗树,则分化成两个链表插入到新的桶中,比如,假如原来容量为4,3、7、11、15这四个元素都在三号桶中,现在扩容到8,则3和11还在三号桶,7和15要搬移到七号桶中,也就是分化成了两个链表
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// (e.hash & oldCap) == 0的元素放在低位链表中
// 比如,3 & 4 == 0
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// (e.hash & oldCap) != 0的元素放在高位链表中
// 比如,7 & 4 != 0
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历完成分化成两个链表了
// 低位链表在新桶中的位置与旧桶一样(即3和11还在三号桶中)
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 原索引+oldCap放到bucket里(高位链表在新桶中的位置正好是原来的位置加上旧容量(即7和15搬移到七号桶了))
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize过程中对Hash表数组大小的修改使用的是2次幂的扩展(指长度为原来的2倍),这样有两个好处
- 1、在hashmap的源码中。put方法会调用indexFor(int h, int length)方法,这个方法主要是根据key的hash值找到这个entry在Hash表数组中的位置,源码如下:
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
上述代码也相当于对length求模。 注意最后return的是h&(length-1)。如果length不为2的幂,比如15。那么length-1的2进制就会变成1110。在h为随机数的情况下,和1110做&操作。尾数永远为0。那么0001、1001、1101等尾数为1的位置就永远不可能被entry占用。这样会造成浪费,不随机等问题。 length-1 二进制中为1的位数越多,那么分布就平均。
- 2、以下图为例,其中图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,n代表length。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

resize过程中不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图(一方面位运算更快,另一方面抗碰撞的Hash函数其实挺耗时的):

3、putTreeVal()方法
//插入元素到红黑树的方法
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
//标记是否找到这个key的节点
boolean searched = false;
//找到树的根节点
TreeNode<K,V> root = (parent != null) ? root() : this;
//从树的根节点开始遍历
for (TreeNode<K,V> p = root;;) {
// dir=direction,标记是在左边还是右边
// ph=p.hash,当前节点的hash值
int dir, ph;
//pk=p.key,当前节点的key值
K pk;
if ((ph = p.hash) > h)
//当前hash比目标hash大,说明在左边
dir = -1;
else if (ph < h)
//当前hash比目标hash小,说明在右边
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//两者hash相同且key相等,说明找到了节点,直接返回节点
//回到putVal()中判断是否需要修改其value值
return p;
else if ((kc == null &&
// 如果k是Comparable的子类则返回其真实的类,否则返回null
(kc = comparableClassFor(k)) == null) ||
// 如果k和pk不是同样的类型则返回0,否则返回两者比较的结果
(dir = compareComparables(kc, k, pk)) == 0) {
// 这个条件表示两者hash相同但是其中一个不是Comparable类型或者两者类型不同
// 比如key是Object类型,这时可以传String也可以传Integer,两者hash值可能相同
// 在红黑树中把同样hash值的元素存储在同一颗子树,这里相当于找到了这颗子树的顶点
// 从这个顶点分别遍历其左右子树去寻找有没有跟待插入的key相同的元素
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
//遍历左右子树找到了直接返回
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
//如果两者类型相同,再根据它们的内存地址计算hash值进行比较
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
//如果最后确实没有找到对应key的元素,则新建一个节点
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 插入树节点后平衡
// 把root节点移动到链表的第一个节点
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
4、treeifyBin方法(插入元素后链表的长度大于8则判断是否需要树化)
/**
*如果hash table的长度大于64,则将指定位置上的所有节点转换为TreeNode;
*否则只对hast table进行扩容
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 如果桶数量小于64,直接扩容而不用树化
// 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
// 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素
// 这样即使扩容也无法减少链表的长度
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//转换为红黑树
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
// 如果进入过上面的循环,则从头节点开始树化
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
5、treeify()方法
//真正的树化
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 第一个元素作为根节点且为黑节点,其它元素依次插入到树中再做平衡
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点查找元素插入的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// 如果最后没找到元素,则插入
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//插入后平衡,默认插入的是红节点,在balanceInsertion()方法里
root = balanceInsertion(root, x);
break;
}
}
}
}
// 把根节点移动到链表的头节点,因为经过平衡之后原来的第一个元素不一定是根节点了
moveRootToFront(tab, root);
}
6、get操作
-
a.bucket里的第一个节点直接命中
-
b.如果由冲突,则通过key.equals(k)去查找对应的entry
-
如果为树,则在树中通过key.equals(k)查找,时间复杂度为o(logn);
-
如果为链表,则在链表中通过key.equals(k查找),时间复杂度为o(n);
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//如果桶的数量大于0并且待查找的key所在的桶的第一个元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//检查第一个元素是不是要查的元素,如果是直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 未命中
if ((e = first.next) != null) {
// 如果第一个元素是树节点,则按树的方式查找
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 否则就遍历整个链表查找该元素
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
7、getTreeNode(int h, Object k)方法
/**
* Calls find for root node.
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//从树的根节点开始查找
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h)
//左子树
p = pl;
else if (ph < h)
//左子树
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//找到了直接返回
return p;
else if (pl == null)
//hash值相同但是key不同,左子树为空查右子树
p = pr;
else if (pr == null)
//右子树为空查左子树
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//// 通过compare方法比较key值的大小决定使用左子树还是右子树
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)
//// 如果以上条件都不通过,则尝试在右子树查找
return q;
else
// 都没找到就在左子树查找
p = pl;
} while (p != null);
return null;
}
8、hash函数的实现
在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示

static final int hash(Object key) {
int h;
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
//其中n是数组的长度,即Map的数组部分初始化长度
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们可以看到,在 hashmap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。
前面说过,hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个。那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以,我们首先想到的就是把 hashcode 对数组长度取模运算。这样一来,元素的分布相对来说是比较均匀的。
但是“模”运算的消耗还是比较大的,能不能找一种更快速、消耗更小的方式?我们来看看 JDK1.8 源码是怎么做的
简单来说就是:
*高16 bit 不变,低16 bit 和高16 bit 做了一个异或(得到的 hashcode 转化为32位二进制,前16位和后16位低16 bit和高16 bit做了一个异或)
*(n·1) & hash = -> 得到下标
9、拉链法导致的链表过深,为什么不用二叉查找树代替而选择红黑树?为什么不一直使用红黑树?
之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
10、为什么链表的长度达到了8就会变成红黑树,而不是6,10,20的长度
HashMap在jdk 1.8后引入了红黑树的概念,表示若桶中的元素个数超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式.
原因:红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
选择6和8的原因是: 中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
11、对红黑树的见解

-
1、每个节点非红即黑
-
2、根节点总是黑色的
-
3、如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
-
4、每个叶子节点都是黑色的空节点(NIL节点)
-
5、从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
12、如果 HashMap 的大小超过了负载因子(load factor)定义的容量怎么办?
HashMap 默认的负载因子大小为0.75。也就是说,当一个 Map 填满了75%的 bucket 时候,和其它集合类一样(如 ArrayList 等),将会创建原来 HashMap 大小的两倍的 bucket 数组来重新调整 Map 大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing。
13、重新调整 HashMap 大小存在什么问题吗?
重新调整 HashMap 大小的时候,确实存在条件竞争。
因为如果两个线程都发现 HashMap 需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来。因为移动到新的 bucket 位置的时候,HashMap 并不会将元素放在链表的尾部,而是放在头部。这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。多线程的环境下不使用 HashMap。
14、多线程会导致死循环(jdk 1.8之前),它是怎么发生的?
HashMap 的容量是有限的。当经过多次元素插入,使得 HashMap 达到一定饱和度时,Key 映射位置发生冲突的几率会逐渐提高。这时候, HashMap 需要扩展它的长度,也就是进行resize。
多线程put数据到HashMap中
public class HashMapInfiniteLoop {
private static HashMap<Integer,String> map = new HashMap<>(2,0.75f);
public static void main(String[] args) {
map.put(3,"A");
new Thread("thread1" ){
@Override
public void run() {
map.put(7,"B");
System.out.println(map);
}
}.start();
new Thread("thread2"){
@Override
public void run() {
map.put(5,"C");
System.out.println(map);
}
}.start();
}
}
jdk 1.8之前的put()方法
public V put(K key, V value) {
......
// 计算 Hash 值
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
// 如果该 key 已被插入,则替换掉旧的 value(链接操作)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 该 key 不存在,需要增加一个结点
addEntry(hash, key, value, i);
return null;
}
检查容量是否超标
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//查看当前的 size 是否超过了设定的阈值 threshold,如果超过,需要 resize
if (size++ >= threshold)
resize(2 * table.length);
}
新建一个更大尺寸的hash表,然后把数据从老的hash表中迁移到新的hash表中
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
......
// 创建一个新的 Hash Table
Entry[] newTable = new Entry[newCapacity];
// 将 Old Hash Table 上的数据迁移到 New Hash Table 上
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
迁移的方法
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
//下面这段代码的意思是:
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
该方法实现的机制就是将每个链表转化到新链表,并且链表中的位置发生反转,而这在多线程情况下是很容易造成链表回路,从而发生 get() 死循环。所以只要保证建新链时还是按照原来的顺序的话就不会产生循环(JDK 8 做了改进)
多线程情况下的transfer 1)、假设有两个线程
do {
Entry<K,V> next = e.next; // 假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
而第二个线程执行完成了,于是就是下面这个样子

注意,因为 Thread1 的 e 指向了 key(3),而 next 指向了 key(7),其在线程二 rehash 后,指向了线程二重组后的链表。可以看到链表的顺序被反转
2)线程一被调度回来执行
- 先是执行 newTalbe[i] = e;
- 然后是 e = next,导致了 e 指向了 key(7)
- 而下一次循环的 next = e.next 导致了 next 指向了 key(3)

3)线程一接着工作。把 key(7) 摘下来,放到 newTable[i] 的第一个,然后把 e 和 next 往下移

4)环形链接出现 e.next = newTable[i] 导致 key(3).next 指向了 key(7) 此时的 key(7).next 已经指向了 key(3), 环形链表就这样出现了

jdk 8的改进 JDK 8 中采用的是位桶 + 链表/红黑树的方式,当某个位桶的链表的长度超过 8 的时候,这个链表就将转换成红黑树 HashMap 不会因为多线程 put 导致死循环(JDK 8 用 head 和 tail 来保证链表的顺序和之前一样;JDK 7 rehash 会倒置链表元素),但是还会有数据丢失等弊端(并发本身的问题)。因此多线程情况下还是建议使用 ConcurrentHashMap
假如一个线程m.containsKey(k)为真,在还没执行m.get(k)的时候,k被另外一个线程给删除了,那么m.get(k)会返回null。如果允许null值的话,就会错误的判断为k还存在;因此不允许null值的话就可以正常的表示出当前的k是不存在的。所以在ConcurrentHashMap不应该有如下的写法,Key和Value不允许null值。 其实Value不允许null值就可以,Key为null似乎没什么影响。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
声明两个指针,维护两个链表,依次在末端添加新的元素。虽然解决了死循环问题,但还是会有其他问题,所以多线程还是尽量用ConcurrentHashMap。
这里比较难理解的是 (e.hash & oldCap) == 0 这一句,但是这个和解决死循环没什么关系。
这里的操作就是 (e.hash & oldCap) == 0 这一句,这一句如果是true,表明(e.hash & (newCap - 1))还会和 e.hash & (oldCap - 1)一样。因为oldCap和newCap是2的次幂,并且newCap是oldCap的两倍,就相当于oldCap的唯一一个二进制的1向高位移动了一位 , (e.hash & oldCap) == 0就代表了(e.hash & (newCap - 1))还会和e.hash & (oldCap - 1)一样。
比如原来容量是16,那么就相当于e.hash & 0x1111 (0x1111就是oldCap - 1 = 16 - 1 = 15),现在容量扩大了一倍,就是32,那么rehash定位就等于e.hash & 0x11111 (0x11111就是newCap - 1 = 32 - 1 = 31)现在(e.hash & oldCap) == 0就表明了e.hash & 0x10000 == 0,这样的话,不就是已知: e.hash & 0x1111 = hash定位值Value并且 e.hash & 0x10000 = 0那么 e.hash & 0x11111 不也就是原来的hash定位值Value吗?
那么就只需要根据这一个二进制位就可以判断下次hash定位在哪里了。将hash冲突的元素连在两条链表上放在相应的位置不就行了嘛。
为什么线程不安全:
HashMap 在并发时可能出现的问题主要是两方面:
- 如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖
- 如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失
二、ConcurrentHashMap源码解读
1、ConcurrentHashMap的key和value不能为null值
ConcurrentHashmap和Hashtable都是支持并发的,这样会有一个问题,当你通过get(k)获取对应的value时,如果获取到的是null时,你无法判断,它是put(k,v)的时候value为null,还是这个key从来没有做过映射。 HashMap是非并发的,可以通过contains(key)来做这个判断。 而ConcurrentHashMap在调用m.containsKey(key)和m.get(key),这两个方法都是没有加锁的,调用时候m可能被其他线程改变了。
if (m.containsKey(k)) {
return m.get(k);
} else {
throw new KeyNotPresentException();
}
2、ConcurrentHashMap的关键属性
ConcurrentHashMap在1.8中的实现,相比较1.7基本上全部都变掉了.而是启用了一种全新的CAS算法的方式实现,Node + CAS + Synchronized。数据结构沿用了与它同时期的HashMap版本的思想,底层依然由数组+链表+红黑树的方式思想。对于锁的粒度,调整为对每个数组元素加锁(Node),然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。下面是其基本结构:

相关属性:
/**
*表初始化和调整控件大小。当为负时,
正在初始化或调整表的大小:初始化为-1,
否则-(1+活动调整大小线程的数目)。否则,
当表为空时,保留要在其上使用的初始表大小
创建,或默认为0。初始化后,保持
下一个要调整表大小的元素计数值
*/
private transient volatile int sizeCtl;
sizeCtl用于table[]的初始化和扩容操作,不同值的代表状态如下:
- -1:table[]正在初始化。
- -N:表示有N-1个线程正在进行扩容操作。
非负情况:
- 如果table[]未初始化,则表示table需要初始化的大小。
- 如果初始化完成,则表示table[]扩容的阀值,默认是table[]容量的0.75 倍。
private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
- DEFAULT_CONCURRENCY_LEVEL:表示默认的并发级别,也就是table[]的默认大小。
private static final float LOAD_FACTOR = 0.75f;
- LOAD_FACTOR:默认加载因子
static final int TREEIFY_THRESHOLD = 8;
- TREEIFY_THRESHOLD:链表转红黑树的阀值,当table[i]下面的链表长度大于8时就转化为红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
- UNTREEIFY_THRESHOLD:红黑树转链表的阀值,当链表长度<=6时转为链表(扩容时)。
构造函数:
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
从上面代码可以看出,在创建ConcurrentHashMap时,并没有初始化table[]数组,只对Map容量,并发级别等做了赋值操作。相关节点:
- Node:该类用于构造table[],只读节点(不提供修改方法)。
- TreeBin:红黑树结构。
- TreeNode:红黑树节点。
- ForwardingNode:临时节点(扩容时使用)。
table
transient volatile Node<K,V>[] table;
装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。
nextTable
private transient volatile Node<K,V>[] nextTable;volatile Node<K,V>[] nextTable; //扩容时使用,平时为null,只有在扩容的时候才为非null
sizeCtl
private transient volatile int sizeCtl;该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况:
当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度;
若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor;
当值为0时,即数组长度为默认初始值。
sun.misc.Unsafe U
private static final sun.misc.Unsafe U;在ConcurrentHashMapde的实现中可以看到大量的U.compareAndSwapXXXX的方法去修改ConcurrentHashMap的一些属性。这些方法实际上是利用了CAS算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。而CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中:
static {
try {
U = sun.misc.Unsafe.getUnsafe();
.......
} catch (Exception e) {
throw new Error(e);
}
}
3、ConcurrentHashMap中关键内部类
Node
Node类实现了Map.Entry接口,主要存放key-value对,并且具有next域
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> 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<K,V> find(int h, Object k) {
Node<K,V> 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;
}
}
另外可以看出很多属性都是用volatile进行修饰的,也就是为了保证内存可见性。
TreeNode
树节点,继承于承载数据的Node类。而红黑树的操作是针对TreeBin类的,从该类的注释也可以看出,也就是TreeBin会将TreeNode进行再一次封装
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K,V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* Returns the TreeNode (or null if not found) for the given key
* starting at given root.
*/
final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk; TreeNode<K,V> q;
TreeNode<K,V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
TreeBin
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
......
}
ForwardingNode
在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
4、CAS关键操作
在上面我们提及到在ConcurrentHashMap中会大量使用CAS修改它的属性和一些操作。因此,在理解ConcurrentHashMap的方法前我们需要了解下面几个常用的利用CAS算法来保障线程安全的操作。
tabAt
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
该方法用来获取table数组中索引为i的Node元素。
casTabAt
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
利用CAS操作设置table数组中索引为i的元素
setTabAt
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
该方法用来设置table数组中索引为i的元素
5、重点方法讲解
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)
//若table未创建,则初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//table[i]后面无节点时,直接创建Node(无锁操作)
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 如果当前正在扩容,则帮助扩容并返回最新table[]
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//在链表或者红黑树中追加节点
V oldVal = null;
//这里并没有使用ReentrantLock,说明synchronized已经足够优化了(1.7使用的ReentrantLock)
synchronized (f) {
if (tabAt(tab, i) == f) {
//如果为链表结构
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
//找到key,替换value
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;
//在尾部插入Node
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)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
从上面代码可以看出,put的步骤大致如下:
- 参数校验。
- 若table[]未创建,则初始化。
- 当table[i]后面无节点时,直接创建Node(无锁操作)。
- 如果当前正在扩容,则帮助扩容并返回最新table[]。
- 然后在链表或者红黑树中追加节点。
- 最后还回去判断是否到达阀值,如到达变为红黑树结构。
get操作:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//定位到table[]中的i
int h = spread(key.hashCode());
//若table[i]存在
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方法的流程相对简单一点,从上面代码可以看出以下步骤:
- 首先定位到table[]中的i。
- 若table[i]存在,则继续查找。
- 首先比较链表头部,如果是则返回。
- 然后如果为红黑树,查找树。
- 最后再循环链表查找。
ConcurrentHashMap的get操作上面并没有加锁。所以在多线程操作的过程中,并不能完全的保证一致性。这里和1.7当中类似,是弱一致性的体现。
size操作:
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
public long mappingCount() {
long n = sumCount();
return (n < 0L) ? 0L : n; // ignore transient negative values
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
JDK1.8中新增了一个mappingCount()的API。这个API与size()不同的就是返回值是Long类型,这样就不受Integer.MAX_VALUE的大小限制了。
两个方法都同时调用了,sumCount()方法。对于每个table[i]都有一个CounterCell与之对应,上面方法做了求和之后就返回了。从而可以看出,size()和mappingCount()返回的都是一个估计值。(这一点与JDK1.7里面的实现不同,1.7里面使用了加锁的方式实现。这里面也可以看出JDK1.8牺牲了精度,来换取更高的效率。)