HashMap分析详解
HashMap分析
本篇先从hashMap讲起,下篇进行ConcurrentHashMap的分析。
数据结构
1.7版本是数组+链表。大于等于1.8版本是1.8 = 数组 + 链表 + 红黑树
容量
HashMap的容量就是数组的大小。
DEFAULT_INITIAL_CAPACITY = 1 << 4;
如果不写构造参数, Hash表默认初始容量:16
如果写了初始容量,如11,就是11吗
HashMap hashMap = new HashMap<Object, String>(11);
答案是接近2的N次方,16
时间复杂度
hashmap的get,put操作时间复杂度O(1),O(1)代表1下就能找到。
结构初认识
不考虑其他因素的话,我们肯定是从头放起,0-15,我们放这些目的是为了取出来。
那么问题来了,下一次取的时候就需要遍历数组。这样就会很慢O(n)。
我们Object类里有hashcode方法。key.hashCode得到的是不确定的值 (有符号的整型值)
那怎么能保证数据落到0-15之内呢,可以进行取模hashCode % 16 的值就是0-15之间。
这样就可以通过索引下标直接拿到值array[index] = value;
那么数组所有的元素位是否能够100%被利用起来?
不一定,会产生hash冲突。
引入链表结构解决hash冲突,采用头部插入链表法,链表时间复杂度O(n)
hash并不是用取模计算index,而是用位运算!因为效率远远高于取模。
重要成员变量
hash表的存放数组。
DEFAULT_INITIAL_CAPACITY = 1 << 4;
Hash表默认初始容量
MAXIMUM_CAPACITY = 1 << 30;
最大Hash表容量
DEFAULT_LOAD_FACTOR = 0.75f;
达到75%就会进行扩容默认加载因子
TREEIFY_THRESHOLD = 8;
链表转红黑树阈值 ,不是长度,是阈值,链表长度大于8
UNTREEIFY_THRESHOLD = 6;
红黑树转链表阈值
MIN_TREEIFY_CAPACITY = 64;
链表转红黑树时hash表最小容量阈值,达不到优先扩容。
为什么是2的幂次
数组的初始容量必须是2的整数次幂,不一定是16.在初始化的时候会进行检查
roundUpToPowerOf2(size),强型将非2的指数次幂的数值转化成2的指数次幂
怎么转化?
1、必须最接近size,11
2、必须大于=size,
3、是2的指数次幂
刚才我们看了并没有用取模运算,而是位运算。
源码分析.计算索引
int i = indexFor(hash, table.length);
static int indexFor(int h, int length) {
// key.hashCode % table.lenth
return h & (table.lenth-1);
}
h & (table.lenth-1)其实就相当于h%table.lenth。那么为什么是table.lenth-1。
假设hashcode是,table.length假设是16
h=0001 0101 0111 0010 1111 &
0000 0000 0000 0010 0000
如果不去减1也就意味着只有2种结果,0和16。也就是只能放到数组第一个和最后1个位置。
中间位置都是空的。并且还会越界
如果减1呢,
0000 0000 0000 0000 1111 //16-1=15
0000 0000 0000 0000 1010
这样的结构就是0-15之间的任意1个数。减少了hash碰撞
如何扩容
当前hashmap存了多少element,假设是size,比对一下threshold,扩容的阈值。
threshold扩容阈值 = capacity * 扩容阈值比率 0.75 = 16*0.75=12。也就是说数组容量是16,当达到12时就考虑扩容
扩容怎么扩?
扩容为原来的2倍。不是随便扩的,必须是2的指数次幂,所以是2倍。
扩容过程
扩容导致的死锁jdk1.7版本
如果是多线程的话就会发生问题
链表成环,死锁问题
因为没有加锁,是线程不安全的。
在jdk1.7版本里,多线程情况下,扩容会让链表成环,造成死锁问题。
原因就是:没进行任何同步操作,并且用到的是头插法。
jdk1.8版本的HashMap
jdk1.8版本引入了红黑树。俩个条件
1只有当数组容量>=64,才会链表转红黑树,否则优先扩容。
2.只有等链表过长,阈值设置TREEIFY_THRESHOLD = 8,不是代表链表长度,链表长度>8,链表9的时候转红黑树
源码:
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
jdk1.8版本的如何解决死锁扩容问题
扩容源码
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {//hashcode与运算扩容前数组大小
//比如yangguo.hashcode & 16 = 0,用低位指针
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//yangguo.hashcode & 16 》 0 高位指针
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;,移到新的数组上的同样的index位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //index 3+16 = 19
}
基于高低位指针实现扩容。分析下:
定义4组高低位的指针,进行扩容
具体扩容过程
完全绕开rehash,要满足高低位移动,必须数组容量是2的幂次方。
应用场景
上面这种做法可以应用到哪些场景?
分库分表,在线扩容
最后分享俩个链接,hashMap通俗易懂的,有兴趣的可以看看