0.序言
? HashMap 在JDK1.7版本时底层数据结构是数组+链表,但是1.7时HashMap的并发扩容会导致死循环;JDK1.8时底层的数据结构变成数组+链表+红黑树,修改了并发扩容逻辑,并且提升了HashMap的性能。
0.1HashMap 重要属性
DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash 表默认初始容量 8 MAXIMUM_CAPACITY = 1 << 30; 最大 Hash 表容量 DEFAULT_LOAD_FACTOR = 0.75f;默认扩容阈值比率(计算扩容阈值=阈值比率*HashMap容量) TREEIFY_THRESHOLD = 8;链表转红黑树阈值 UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值 MIN_TREEIFY_CAPACITY = 64;链表转红黑树时 hash 表最小容量阈值,达不到优先扩容
1.0 JKD1.7HashMap组成结构及源码分析
HashMap 底层数据结构是数组+链表:数组就是存放Map 键值对的位置,当Map 中出现相同的Hashcode值时,称作hash碰撞,这时相同位置的值会形成一个链表放在数组的特定位置。如下图
1.1如何确定key在数组的位置
源码如下:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
中的 hash 方法源码如下:
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
? 不需要看懂,知道返回一个int 类型的hash值就可以了;怎么一个数值 中获取一个小于数组长度的int值呢?我们可能会想到直接对那个数值求余% (hash值%数组长度),有实验可知求余运算效率低,HashMap是不会用的,我看下它到底是怎么计算的呢?源码如下用了&运算。
static int indexFor(int h, int length) {
return h & (length-1);
}
1.2 HashMap的容量
为了保HashMap的数组长度必须是2的N次方,HashMap在初始化时的长度为11,数组的长度就是11吗?不是11是16,为什么呢?看以下源码
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
初始化方法源码:
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
public static int highestOneBit(int i) {
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
1.3 HashMap 扩容
当存入的数据长度 大于 数组长度*阈值率,HashMap自动扩容,扩容后数据的迁移逻辑如下:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
重点看注释写第N行的代码,这样逻辑更加清晰了。
第一行:记录oldhash表中e.next; 第二行:rehash计算出数组的位置(hash表中链表的位置) 第三行:e要插入链表的头部,所以要先将e.next指向newhash表中的第一个元素; 第四行:将e放入到newhash表的头部 第五行:转移e到下一个节点,继续循环下去;
1.3.1 单线程扩容
假设:hash算法就是简单的key与lenath(数组长度)求余。hash表长度为2,如果不扩容,那么元素kev为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,kev=5会落到table[1]上(5%4=3),当前e.next为null,跳出while循环,resize结束。途中竖着是数组,横着的是链表
1.3.2多线程扩容
多线程同时put的情况,然后同时进入transfer方法中:假设这里的两个线程同时执行了put操作,并进入了transfer如下环节。
while(null != e) {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
从上面的图我们可以看到,因为线程1的e指向了key(3),而next指向了key(7),在线程2rehash后,就指向了线程2 rehash后的链表。然后线程1被唤醒了: 1.执行e.next=newTablelil,干是kev(3)的next指向了线程1的新Hash表,因为新Hash表为空,所以e.next=null 2.执行newTable[i]=e,所以线程1的新Hash表第一个元素指向了线程2新Hash表的key(3)。好了,e处理完毕。 3.执行e=next,将e指向next,所以新的e是key(7)
然后该执行key(3)的next节点key(7)了: 1.现在的e节点是key(7),首先执行Entry<K,V>next=enext,那么next就是key(3)了 2.执行enext=newTable],于是key(7)的next就成了key(3) 3.执行newTable[]=e,那么线程1的新Hash表第一个元素变成了key(7) 4.执行e=next,将e指向next,所以新的e是key(3)
此时状态为:
然后又该执行key(7)的next节点key(3)了: 1.现在的e节点是kev(3),首先执行Entrynext=enext那么next就是null 2.执行enext=newTable[i],于是key(3)的next就成了key(7) 3.执行newTable[i]=e,那么线程1的新Hash表第一个元素变成了kev(3) 4.执行e=next,将e指向next,所以新的e是key(7)
很明显,出现了环形的死循环。
1.4 Jdk1.8 HashMap的优化
? 优化了扩容逻辑,避免了死循环;添加红黑树,在链表数据量大时查询速度提升。
1.4.1 扩容优化
扩容的关键代码:
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;
}
else if (oldThr > 0)
newCap = oldThr;
else {
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 {
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;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这里定义了四个指针,将某个链表分为两部分,链表节点和数组长度相与的结果作为分隔,等于0的放在以loHead为头节点的链表中,等1的放在以hiHead为头节点的链表中。如下图:
? 这种移动方式没有改变节点关系的方向,所以并发之下也没有问题 。
|