Java面试& HashMap实现原理分析
美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析
HashMap1.8之后为什么要采用数组+链表+红黑树的储存方式?
链表是一种物理存储单元上非连续、非顺序的存储结构,有多个节点组成,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。特点是插入快,查找慢,和数组相反。
因为一开始数据存数组如果发生hash冲突,这个时候需要把冲突的数据放到后面的链表中(链地址法),如果hash冲突的数据过多,就会让链表过长,查询效率会变低,所以jdk1.8之后当链表长度大于8时就是转化为红黑树。其中换会牵涉到一个数组扩容,
HashMap常见面试题解析
数组+链表(好扩容next a[][]) , 数组+链表+红黑树
通过获取key对象的hashcode计算出该对象的哈希值,通过改哈希值与数组长度减去1进行位与运算(n-1 & hash),得到buckets 的位置,当发生hash冲突时,如果value值一样,则会替换旧的key的value,value不一样则新建链表结点,当链表的长度超过8,则转换为红黑树存储。
在jdk1.8之前创建该对象,会创建一个长度为16的Entry[] table用来存储键值对数据。jdk1.8之后不是在构造方法创建了,而是在第一次调用put方法时才进行创建,创建Node[] table,然后java7中链表的加入时
Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。
但是即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。
HashMap会进行resize(扩容)操作,重新计算hash值,在resize操作的时候会造成线程不安全。下面将举两个可能出现线程不安全的地方。
1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%),即产生链表循环引用的现象(jdk7)
currentHashMap 以及 hashTable
默认的初始化大小是16 原因是这样的,如果桶初始化桶数组设置太大,就会浪费内存空间,16是一个折中的大小,既不会像1,2,3那样放几个元素就扩容,也不会像几千几万那样可以只会利用一点点空间从而造成大量的浪费。
大小为2的幂是,在计算buckets桶位置的时候,公式为((n-1) & hash),2的幂减去1的数的二进制数的结尾都是1,与hash值进行与运算,会得到其余数。进行按位与操作,使得结果剩下的值为对象的hash值的末尾几位,这样就我们只要保证对象的hash值生成足够散列即可
使存储高效,尽量减少碰撞,在((n-1)&hash) 求索引的时候更均匀
数组长度是2的n次幂时
数组长度 不是2的n次幂时
加载因子设置为0.75而不是1,是因为设置过大,桶中键值对碰撞的几率就会越大,同一个桶位置可能会存放好几个value值,这样就会增加搜索的时间,性能下降,设置过小也不合适,如果是0.1,那么10个桶,threshold为1,你放两个键值对就要扩容,太浪费空间了。
//默认的map大小,为2的n次幂
static final int DEFAULT_INITIAL_CAPACITY(默认初始容量) = 1 << 4; // aka 16
// 最大容量,指定的值超过 2的30次幂,默认使用这个值
static final int MAXIMUM_CAPACITY (最大容量)= 1 << 30;
//在构造函数中没有指定时使用的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表长度为8时,使用以红黑树代替链表,红黑树的结点是链表长度的两倍,当比较短的时候,使用红黑树的效率其实并不高,根据泊松分布公式的统计结果,在结点数达到8时,适合使用红黑树
static final int TREEIFY_THRESHOLD(恐吓的阈值) = 8;
// 红黑树转为链表的阈值
static final int UNTREEIFY_THRESHOLD (非恐吓的阈值)= 6;
//链表转红黑树时数组的大小的阈值,即数组大小大于这个数字时且链表长度大于8才会转为红黑树,
//在数组长度小于64,不会转,而是进行扩容
static final int MIN_TREEIFY_CAPACITY = 64(小的恐吓容量);
如果key相同,则会替换key对应的内容最最小值,key不相同,则接到后面的链表,如果链表长度到达8且数组的长度大于64时,则将链表转为红黑树,如果数组长度小于64,则是进行扩容
将对象的hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列
在get和put的过程中,计算下标时,先对hashCode进行hash操作,然后再通过hash值进一步计算下标,如下图所示:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
目的:将值的二进制数,从左到右的第一个出现的1开始,右边的所有值都变成1,使得可以找出比当前值大一点点的2的n次幂的数
当执行 n >>> 16 时,即意味着将32位数都进行了一次按位或运算,将
时间复杂度O(1)
无法进行范围查找
回表?
二分类似
两个红节点不能相连
//新建b数组
1.将index前的直接a数组赋值b数组
2.将插入的value赋值给index
3.将index后的值a[i-1]数组赋值b数组
public static int[] insert(int a[], int index, int value) {
int b[] = new int[a.length + 1];
for (int i = 0; i < b.length; i++) {
if (i < index - 1) {
b[i] = a[i];
}
if (i == index - 1) {
b[i] = value;
}
if (i > index - 1) {
b[i] = a[i - 1];
}
}
return b;
//底层是双向链表 next,prev
Node head = new Node(“data”);
head.next = new Node(“data1”);
hashcode:字符串转ascll码,数组取模,算出哈希表的坐标
缺点:hash碰撞,冲突(可能数据的ascll相同,后面的会覆盖前面的值)
解决:链表==》 next指针,如果相同指向下一个next
1.创建一个Entry对象,{key,value,next}
2.将对象存储在数组中,put时根据hash值(不同)取模算出坐标
3.如果坐标不为null,将通过链表下标存储
put方法,get方法
查询快(二分查找),插入慢(会左旋 132)