HashMap源码阅读(java1.8.0)
1.1.1 红黑树
二叉查找树可能因为多次插入新节点导致失去平衡,使得查找效率低,查找的复杂度甚至可能会出现线性的,为了解决因为新节点的插入而导致查找树不平衡,此时就出现了红黑树。
红黑树它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。它具有以下特点:
(1)每个节点或者是黑色,或者是红色。
(3)每个叶子节点(叶子节点,是指为空(NIL或NULL)的叶子节点)是黑色。
(4)如果一个节点是红色的,则它的子节点一定是黑色(即从根节点到叶子节点的路径上不能有两个重复的红色节点)。
(5)从一个节点到其上每一个叶节点的所有路径都具有相同的黑色节点个数。
红黑树的基本操作--添加
① 将红黑树当作一颗二叉查找树,将节点插入。
② 将插入的节点着色为"红色"。(因为条件5,从一个节点到其中每一个节点的的所有路径都具有相同的黑色节点)。
③通过一系列的旋转(左旋或右旋操作)或着色等操作,使之重新成为一颗红黑树。
在java 1.7之前是用数组和链表一起组合构成HashMap,在java1.8之后就使用当链表长度超过8之后,就会将链表转化为红黑树,缩小查找的时间(红黑树维护也会花费大量时间,包含左旋、右旋和变色过程)。
hashmap构造函数会初始化三个值:
- 初始容量initialCapacity:默认值是16,当储存的数据越来越多的时候,就必须进行扩容操作。
- 阈值threshold:hashmap的数组结构中所能存放的最大数量,超过该数量,则会对数组进行扩容。阈值的计算方式为:容量(initialCapacity)*负载因子(loadFactor)。
在初始化一个hashmap对象的时候,指定键值对的同时,也可以指定初始map的容量大小,假设此处我们指定大小为11,则会在构造函数中调用tableSizeFor将容量改为2的n幂次,即比当前容量大,而且必须是2的指数次幂的最小数,就会变成16。这是因为2的指数次幂便于计算进行位运算操作,提升运行效率问题(位运算>加法>乘法>除法>取模)。
hashmap的的默认值是16,负载因子默认是0.75,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //HashMap<String,String> hashMap = new HashMap<String, String>(11); /** * Returns a power of two size for the given target capacity. **/ static final int tableSizeFor( int cap) { int n = cap - 1 ; //10 防止在cap已经是2的n次幂的情况下 // >>> 表示不关心符号位,对数据的二进制形式进行右移 |表示或运算 n |= n >>> 1 ; //15 n |= n >>> 2 ; //15 n |= n >>> 4 ; //15 n |= n >>> 8 ; //15 n |= n >>> 16 ; //15 return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; //16 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | //hashMap.put("2020", "good luck"); /** * Implements Map.put and related methods. * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal( int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果hashtable没有初始化,则初始化该table数组 if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; //通过位运算找到数组中的下标位置,如果数组中对应下标为空,则可以直接存放下去 if ((p = tab[i = (n - 1 ) & hash]) == null ) tab[i] = newNode(hash, key, value, null ); else { //数组元素对应的位置已经有元素,产生碰撞 Node<K,V> e; K k; //如果插入的元素key是已经存在的,则将新的value替换掉原来的旧值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果此时table数组对应的位置是红黑树结构,则将该节点插入红黑树中 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal( this , tab, hash, key, value); else { //如果此时table数组对应的位置是链表结构 for ( int binCount = 0 ; ; ++binCount) { //遍历到数组尾端,没有与插入键值对相同的key,则将新的键值对插入链表尾部 if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); //链表过长,将链表转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1 ) // -1 for 1st treeifyBin(tab, hash); break ; } //发现链表中的某个节点有与插入键值对相同的key,则跳出循环,在循环外部重新赋值 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } //该key在hashmap已存在,更新与在链表跳出循环节点对应的值 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 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /** * 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; //table数组不为空,且对应的下标位置也不为空。 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1 ) & hash]) != null ) { //如果第一个位置是对应的key,则返回 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 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | /** * Initializes or doubles table size. If null, allocates in * accord with initial capacity target held in field threshold. * Otherwise, because we are using power-of-two expansion, the * elements from each bin must either stay at same index, or move * with a power of two offset in the new table. * * @return the table */ final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; //table不为空,且容量大于0 if (oldCap > 0 ) { //如果旧的容量到达阈值,则不再扩容,阈值直接设置为最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //如果旧的容量没有到达阈值,直接操作 else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; // double threshold } //阈值大于0,直接使用旧的阈值 else if (oldThr > 0 ) // initial capacity was placed in threshold newCap = oldThr; //如果阈值为零,则使用默认的初始化值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = ( int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } 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 ) { //依次遍历旧table中每个数组桶的元素 for ( int j = 0 ; j < oldCap; ++j) { Node<K,V> e; //如果数组桶中含有元素 if ((e = oldTab[j]) != null ) { //将下标数据清空 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); //链表 -- 对旧桶里链表中的每一个元素重新计算哈希值得到下标 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 对hash取模运算, * 虽然数组大小扩大了一倍, * 但是同一个key在新旧table中对应的index却存在一定联系: * 要么一致,要么相差一个 oldCap。 */ 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; } |
此处在处理链表的时候,如何将链表中的节点重新分配到新的哈希表需要做一些解释。在扩容的时候,将原来的哈希表扩大了一倍,原来属于同一个桶中的数据会被重新分配,此时取模运算时(a mod b),会注意到,b会扩大两倍(a mod 2b),此时如果该桶中的某一个数据的哈希值是c1(0<c<b),则它必定还是会落入原来的位置,而如果桶中的某一个数据的哈希值是c2(b<c2<2b),则它会被重新分配到一个新的位置(这个位置是原先的哈希桶位置+旧桶的大小)。
HashMap在多线程的情况下出现的死循环现象
在某些java版本中扩容机制如果使用链表,且再插入时使用尾插法会出现死循环,具体原因可以参考老生常谈,HashMap的死循环,在本文中所参考的java版本使用了头插法的方式将元素添加到链表当中,可以避免死循环的出现,但是会出现一部分节点丢失的问题。如图:
假设原始的哈希map的某个桶的数据如下,此时线程开始扩容,将桶中的数据分配到lo和hi桶的链表中。
初始时刻线程1和线程2开始运行,线程1在执行完以下代码后,线程1的时间片运行结束。线程1运行的结果如图所示
线程2与线程1同时运行,线程2的时间片未用完,还在继续执行,根据代码的分配策略,线程2直到时间片运行结束,出现如图所示的结果:
此时CPU的时间片又被分配到了线程1,线程1继续运行,因为此时A所在的链表结构已经发生了变化,只能处理A,B,D三个元素。此时线程1创建的hashmap如图:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验