标签:jdk1.8 hash HashMap 16 链表 源码 线程 数组 浅析
一、设计初衷
数组查询效率高,增删效率低,链表反之,HashMap为了考虑查询、增删效率综合,考虑用数组,和链表,但由于链表长度过长,感性角度,影响美观,其次要查询最末节点值,需要遍历的链表节点很长很多,不能无限制的让加长,因此链表长度超过8时,jdk1.8后引入了红黑树
上图每个框框元素是什么呢?hashMap.put(“a”, 1)时,key,value是核心,不妨可以把key,value两者作为一个整体存储到对应的节点中。
面向对象开发思想,HashMap设计出内部类Node,
链表维护:
数组维护:成员变量 Node<K,V>[] table
二、源码分析
前提:HashMap中一开始是什么都没有的,先形成数组,然后形成链表,在形成红黑树的
1 初始化数组源码分析
当调用put方法时,首先判断成员变量 Node<K,V>[] table 中table数组是否为空
如果数组长度为空,调用resize()方法初始化数组
2 数组下标判断源码分析
前提:
1.先得到一个整形数值 key.hashCode(), Object对象中的native方法,返回整形数值,而且是32位的
2.整形数值范围控制在【0,15】 hashCode %16 构造函数未传参数时数组默认值是16
左位移运算符:1<<4 二进制1,向左移4位 二进制10000=2的4次方(源码不直接写十进制16,因为计算机底层计算都是二进制,写16又去转二进制没必要)
put 是先判断放的位置是否有元素,没有可直接放,有元素需想一想该怎么放
下面来分析源码,判断没有元素,new一个新的节点存入的判断方法
代码解析:tab[i = (n - 1) & hash]
n :数组的大小(构造函数传参的,或默认值16 must be a power of two)
hash : Node节点中 key.hashCode()的值 230072477 32位类型整数 (优化16^16)00101010101010110101000011101011
hash%n 等价 (n - 1) & hash
n-1 二进制运算 10000(16的二进制)-1=01111:
此时(n-1)&hash
00101010101010110101000011101011 操作数1
01111 & 操作数2
范围 00000-01111 之间 即 【0-15】
即 hash%n也是【0-15】即 hash%n 等价 (n - 1) & hash
由以上计算推出,操作数2除最高位是0,其余位是1,其实不参与运算,结果取决于操作数1,来自于hash结果,非常重要
因此hash key.hashCode() 只有最后几位参与运算,为了不让只有最后几位参与运算,优化:让每一位尽可能参与运算
0010101010101011 高16位
0101000011101011
hash----0与1几率相同,由下图可以看出异或最均匀
由以下源码也可以看出put时,先调用了hash函数,对key.hashCode() 向右位移16位,相当于把高16位挤到了低16位即16^16
上面 n-1得到 操作数2(01111) 不参与运算即需保证操作数2除第一位数为0,后面的位数必须全为1,否则操作数1算的不均匀就前功尽弃,那么该怎么保证呢?
由于100000-1=011111,1000000后面不管有多少个0,其n-1的结果永远是第一位数是0,其余位数全为1
那么又如果保证n的第一位数是1,其余位数是0呢?也就是说数组大小如何保证10000…呢,则看源码提示must be a power of two 即指定必须是2^n,即2的4次方为16(数组的初始容量的默认值)
如果我们HashMap构造函数初始化容量传参20偏偏不是2的次方呢,不满足操作数2的规律了,怎么办,继续看源码分析
即使你初始化容量20捣乱,会执行tableSizeFor(20)进行调整成最解决的2的n次幂
4.存入数据源码分析
put方法时,当key相同,新值把oldValue值覆盖,并把旧值返回
key不同
key节点类型 TreeNode,树形结构,用红黑树的方式调用putTreeVal方法进行遍历存储值
链表结构,核心找到链表的尾节点,Node放在后面,尾节点特征,不在有下一个next指向,
因此需要循环遍历当前数组下面的链表,直到找到下一个next节点为空,才能进行放新元素即尾插法(但jdk1.7是头插法,头插法容易形成死循环,整体移动距离较多)
插元素, 当链表长度>=8时 转换为红黑树
疑惑:数组默认16,数组的大小会不会不够用呢?
数组扩容 界限 12=16 * 0.75 即limit=初始容量*加载因子 当存储的数据量达到12啦,就开始扩容 代码如图
初始化、扩容resize()判断条件理解
扩容数组移动分析
当前next指针为空,用新的容量,hash&n-1
是红黑树 split打散,进行新的分配
是链表,需循环遍历链表 ,将链表移到新的位置,要么是原来的位置,要么是原来位置+原容量的位置
三、HashMap总结
1 HashMap中put方法过程
- 对key求hash值,然后计算下标
- 如果没有碰撞,直接放入桶中
- 如果碰撞了,以链表的方式链接到后面
- 如果链表的长度超过8,则链表转换为红黑树
- 如果节点已存在就替换旧值
- 如果桶满了(容量*加载因子),需要扩容
2 HashMap中hash函数是怎么实现的
- 高16bit不变,低16bit和高16bit做了一个异或
- (n-1)&hash得到下标
3 HashMap扩容
- 容量扩容为原来的2倍(HashMap的初始容量都是2的n次幂),然后对每个节点重新计算hash值
- 值只可能两个地方,一是原下标位置,二是原下标+原容量的位置
四、ConcurrentHashMap线程安全
1 数组初始化
HashMap没有保证线程安全,HashTable直接在每个方法加synchronized,太简单粗暴不可取,需细化
ConcurrentHashMap 数组初始化 CAS无所化机制线程安全的保障:拿的都是线程中的最新的值,当前内存中的值与线程中的值进行比较
2 put值
检查数组某个位置是否为null,为null,采用CAS无所化机制,插入,只能有一个线程操作
检查数组某个位置不为null,synchronized (当前数组下标节点),不会影响其它数组下标的锁机制,把锁的粒度降低
3 扩容
进行扩容,其它线程都不插入数据(插入数据也没有意义),并且任意线程过来,别闲着,帮助其它线程去扩容,tash任务进行分解,每个线程进来领取自己的任务,在自己的任务管辖范围内,进行对应的扩容操作
标签:jdk1.8,hash,HashMap,16,链表,源码,线程,数组,浅析 来源: https://blog.csdn.net/zhanlijuan2015/article/details/118248452
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。