HashMap底层原理
JDK1.7
put方法
存的是键值对对象
第一步:初始化一个数组(第一次需要初始化,后面不需要)
第一次放元素的时候还没有开辟空间,默认值为16(这个值一定是2的幂次方)
第二步:判断,如果key==null,走putForNullkey方法
在talbe[0]链表中查找key为null的元素,如果找到,则将value重新赋值给这个元素的value,并返回原来的value。如果没找到,则将这个元素添加到talbe[0]链表的表头。
第三步:根据key的值计算一个哈希值
h右移动再异或的原因:让高位参与运算,防止计算出的数组下标相同的太多(扰动函数)
第四步:根据哈希值和数组容量计算数组下标
与运算,算出一个数组下标(与运算比取余要快)
与运算的过程:二进制运算,都为1才为1,否则为0
这就是为什么数组容量一定要是2的幂次方,这样才能保证计算出的数组下标是合法的
计算结果与高位无关,等于传入的hash的最后四位(所以要在上一步计算hash的时候,对hashCode进行位移和异或让高位参与运算)
第五步:遍历当前数组下标的链表,判断是否已经存在相同的key
如果存在,就把当前元素的值覆盖,并返回之前的值
哈希值相等不一定key相同,但哈希值不等key一定不相同
第六步:添加元素(头插法)
1 扩容
1.1判断是否需要扩容
条件一:size大于扩容的阈值,threshold默认为12(16*0.75)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//加载因子=填入哈希表中的数据个数/哈希表的长度
条件二:当前的要放入的位置是空的
1.2扩容的方法
创建一个新的数组,长度是原来的两倍
1.2.1 转移元素
如果扩容前:数组容量为16,元素的数组下标为5
那么扩容后:数组容量为32,元素的数组下标为5+16=21或者还是5
扩容转移元素后,链表的顺序会倒过来,出现线程安全问题!
1.2.2重哈希
第一步:判断是否需要重哈希
这个值可以自己配,如果没有配,就是默认值。一般不用配
2.添加:头插法+向下移动
用e保留现在的头节点
把创建新节点的Entry对象,并放到数组中作为头节点,e作为新节点Entry对象的next域的参数传递
get方法
JDK1.8
put方法
当链表长度大于8并且数组长度大于64时,才会转换为红黑树。如果链表长度大于8,但是数组长度小于64时,还是会进行扩容操作,不会转换为红黑树。在红黑树的元素小于6的时候会变成链表。
链表转红黑树的原因:TreeNodes的大小大约是普通节点的两倍,最开始使用链表的时候,空间占用是比较少的,而且由于链表短,所以查询时间也没有太大的问题。可是当链表越来越长,需要用红黑树的形式来保证查询的效率。
阈值为8的原因:通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率,把长度 8 作为转化的默认阈值。
第一步:初始化一个数组(第一次需要初始化,后面不需要)
第二步:插入节点(尾插法)
判断该元素的数组下标处是用的红黑树还是链表:
如果是树,遍历红黑树,并加入元素
如果是链表,遍历链表,并用尾插法加入元素(计算链表的长度,判断是否有相同的键)
第三步:扩容
1.7中ConcurrentHashMap底层原理
HashMap线程不安全的原因:
后执行的key和value会把先执行的key和value覆盖
头插法也会出现线程不安全
HashTable线程安全的原理:
把这个entry对象本身作为锁,在操作之前先获取锁,操作完之后再释放锁
效率不高的原因:两个线程的index可能不同,但却要等
分段锁,以Segment对象为锁
初始化后,并发度不会改变了,即Segment数组不会扩容
每个Segment下面的Entry数组的容量相同
扩容的时候是扩容的Entry数组