JDK1.7中的HashMap
重要成员属性
// 默认初始容量 初始容量必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大hash表容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子(取了在时间和空间上一个比较不错的均衡,不一定是0.75,不同的hashmap实现也存在不同的值)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 空hash表
static final Entry<?,?>[] EMPTY_TABLE = {};
// hash表,需要扩容长度必须始终是 2 的幂。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
构造函数
Map<String, String> map = new HashMap<String, String>();
或
Map<String, String> map = new HashMap<String, String>(11);
如果不传入参数默认的初始容量为16,加载因子为0.75。
如果带有初始化参数,上面说过初始容量必须是2的次幂,但是传入的如果不是2的次幂呢?
put()方法
在hashmap的put()方法中,首先会判断如果为空的hash表,则会进行初始化操作。
在进行初始化操作的时候,会对传入的初始容量进行转换成离传入参数最近的那个2的次幂的数值。
接着往下,会对key进行求hash值。
长度必须是 2 的非零次幂,这里为什么要用length-1?
h = 0001 0101 0111 0010 1111
length = 0000 0000 0000 0001 0000 (16不减1) 结果只有两种0,16
length = 0000 0000 0000 0000 1111 (16减1=15) 结果有16种,更加散列
紧接着,for循环,对相同的key的value会进行替换。
接着就进入重要的方法里面,在这里会进行对hash表的扩容。
判断当前hash表的容量,是否大于阈值threshold(capacity * 扩容比率0.75),就进行扩容。扩容是以当前hash表的容量* 2的方式进行扩容。即创建一个新的数组。
扩容后,有了新的数组,就要开始转移数据,在transfer()方法中实现。
在transfer()方法中转移数据,循环遍历需要重新对key进行hash计算,也就是key在旧的数组中的位置与新的数组中的位置不一定相同。然后将旧的数组中的数据移动到新的数组中去。
单线程扩容
假设:hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩 容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上。
扩容:hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前 e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞, 这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句 中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上 (5%4=3), 当前e.next为null, 跳出while循环,resize结束。
多线程扩容
while(null != e) {
Entry<K,V> next = e.next;//第一行,线程1执行到此被调度挂起
int i = indexFor(e.hash, newCapacity);//第二行
e.next = newTable[i];//第三行
newTable[i] = e;//第四行
e = next;//第五行
}
从上面的图可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程 2 rehash 后,就指向了线程2 rehash 后的链表。然后线程1被唤醒了:
- 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因 为新 Hash 表为空,所以e.next = null,
- 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。
- 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
然后该执行 key(3)的 next 节点 key(7)了: - 现在的 e 节点是 key(7),首先执行Entry next = e.next,那么 next 就是 key(3)了
- 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
- 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7) 4. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)
此时状态为:
然后又该执行 key(7)的 next 节点 key(3)了: - 现在的 e 节点是 key(3),首先执行Entry next = e.next,那么 next 就是 null
- 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
- 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
- 执行e = next,将 e 指向 next,所以新的 e 是 key(7)
这时候的状态如图所示:
出现了环形链表。
JDK1.8中的HashMap
重要成员属性
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 链表转红黑树时hash表最小容量阈值,达不到优先扩容
static final int MIN_TREEIFY_CAPACITY = 64;
在jdk1.8中,对hashmap进行了优化,引入了红黑树,在链表过长的时候,也就是阈值为8,不是代表链表的长度,表示链表长度大于8的时候也就是9的时候转红黑树,而且转红黑树还有一个条件,即数组容量大于等于64的时候才会转红黑树,否则优先扩容。为什么链表转红黑树的阈值是8?
(exp(-0.5) * pow(0.5, k) / factorial(k)).
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
泊松分布,链表达到8的概率比较低,因此是8。
put()方法
对比可以发现,jdk1.8对hashmap进行了优化。先来看看转红黑树的条件。
可以发现,如果数组的长度小于64则优先进行扩容。Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,采用高低位拆分转移方式,避免了链表环的产生。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 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
}
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) {
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;
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; //移到新的数组上的同样的index位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
从源码看会发现完全绕开rehash操作,但是要满足高低位移动,必须数组容量是2的幂次方。
如图所示:
jdk8中put方法过程图: