以下HashMap源码的解析都是基于java8来讲解的。
HashMap的结构是数组加链表的形式(jdk7中也是),在java8中引入了红黑树,由于红黑树的时间复杂度是O(log n),引入红黑树是为了解决在哈希冲突很严重的时候导致链表太长,从而引起的查找效率太低的问题。
常量
// HashMap的默认初始容量为16,且容量必须为2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// HashMap的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// HashMap默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化的阈值,当链表长度超过该值时,链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当链表的长度小于6时,红黑树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 树化的前提是capacity大于MIN_TREEIFY_CAPACITY值
static final int MIN_TREEIFY_CAPACITY = 64;
※ 为什么链表长度大于8才进行树化
源码中有下面一段解释
* Because TreeNodes are about twice the size of regular nodes, we
* use them only when bins contain enough nodes to warrant use
* (see TREEIFY_THRESHOLD). And when they become too small (due to
* removal or resizing) they are converted back to plain bins. In
* usages with well-distributed user hashCodes, tree bins are
* rarely used. Ideally, under random hashCodes, the frequency of
* nodes in bins follows a Poisson distribution
* (http://en.wikipedia.org/wiki/Poisson_distribution) with a
* parameter of about 0.5 on average for the default resizing
* threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
大体意思就是:由于TreeNode所占空间是普通Node的两倍,所以只有在bin包含足够多的node的时候才会转化为Tree(有TREEIFY_THRESHOLD决定)。当bin变的很小的时候,又会转为链表。当hashCode分布良好的时候,几乎用不到tree,hash冲突很小。但是在随机hashCode时,离散性差,会导致hash冲突严重,所以导致链表很长,这时候就要转化为红黑树来提高查询效率。按上面java官方给出的概率,链表长度达到8的概率是0.00000006,是很低很低的概率了,所以java也是通过大概率统计得出大于8的时候才转化为红黑树。
内部类 Node
从java8开始,HashMap的节点改为了使用Node(java7使用Entry),其实都差不多,内部结构都类似。
- hash hash值
- key key值
- value value值
- next 下一个节点
方法
-
put方法
put方法调用了下面的putVal方法,这里和java7版本的不同有两处:
- 当链表长度大于8的时候,会转化为红黑树;而且如果Node是TreeNode类型,则按照红黑树的方式进行put
- put采用的是尾插法,这样在扩容的时候避免了多线程情况下出现死循环(java7采用头插法,会存在扩容时出现死循环)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果table为空,则先初始化这个table if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 判断key在table[]中的位置是否有值,如果没值,直接构造Node放在这个位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //有值的情况 //此时p为Node的头 Node<K,V> e; K k; //这里先判断了一下p是不是和要插入的元素hash相同、key相同 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //树化 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //循环Node的next,判断hash和equal是否相同 for (int binCount = 0; ; ++binCount) { //p.next==null说明到链表结尾了,此时构造新Node值放到了p.next //可以看出这里是尾插法(java7是头插法) if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //链表长度超过8的时候转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //hash相同且equal时跳出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //存在hash相同且key值equal的值时,覆盖value值 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //超过阈值时,扩容 if (++size > threshold) resize(); //留给子类实现 afterNodeInsertion(evict); return null; } -
get方法
get方法调用了getNode方法
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { //检查node的头是否是要查找的元素 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { //如果是TreeNode,则使用红黑树的方式查找 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //循环去判断node的next是否是要找的元素 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
HashMap扩容
HashMap的扩容存在的问题也是面试中经常问到的,首先来看下扩容的源码
final Node<K,V>[] resize() {
//...省略数组长度的计算
threshold = newThr;
// 构造出新的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//循环老的数组,重新计算在数组中的位置
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// #1
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
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是java8优化的地方,在下面会详细介绍
//构造新的Node链
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;
}
}
}
}
}
return newTab;
}
在这段代码中,java8进行了优化的,一部分是元素位置不变的,一部分是元素位置+old capacity。它是这样实现的:e.hash & oldCap 这段代码不是为了判断元素所在的位置,而是判断hash值在old capacity的那一位是不是0,举个例子:hashCode是20,old capacity是16,new capacity是32,计算元素位置的方式:(n - 1) & hash
扩容前:
hashCode:00010100
n-1: 00001111
元素位置: 00000100
扩容后:
hashCode:00010100
n-1: 00011111
元素位置: 00010100
扩容前后对比发现,差距就在old capacity二进制1的位置那,所以e.hash & oldCap可以判断出那个位置是否为1。如果为1,那么元素的新位置就是old capacity+扩容前的元素位置 即为扩容后的位置;如果为0,则扩容后的位置与扩容前相同。java8之所以这样优化,首先是因为HashMap的capacity始终是2的幂次方,这样就保证了e.hash & oldCap的正确性;其次这样优化去掉了扩容时的重新hash运算,提高了效率。
扩容存在的问题
-
数据覆盖
看resize源码可以发现,当两个线程同时走到#1的位置时,如果线程1和线程2的key的hash值相同,那线程1赋值后让出cpu,此时线程2获得时间片,同样也进行了赋值操作,那么线程1赋的值就被线程2给覆盖掉了。
-
死循环
在java8中已经不存在死循环了。但是在java7中是存在的。有与java7是头插法,所以在resize的时候,链表的顺序会反转,此时两个线程同时进行resize,就有可能形成链式的圆圈,造成死循环。
HashMap线程不安全,那该用什么
HashMap的线程版本有HashTable和Collections.synchronizedMap(map),但是这两个都是直接加synchronized实现的,效率很低。而HashMap的线程安全版本常用的是ConcurrentHashMap,这个也是面试中经常问到的。明天会更新ConcurrentHashMap的相关面试点,欢迎大家继续关注。
更多精彩内容,欢迎关注我的公众号:猿圈话源 也可以关注我的百家号:猿圈话源
