HashMap原理分析-3(JDK1.7 & 对比1.8)
提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!
HashMap原理分析-3(JDK1.7)
JDK7多线程情况,HashMap扩容可能会出现循环链表问题:
解决多线程出现的问题:
- 如果HashMap的长度提前就能决定,可以初始化的时候就指定足够的长度,防止扩容
- 在上层调用HashMap put()方法的位置加锁
- 使用HashTable(效率比较低)
- 使用ConcurrentHashMap(分段加锁)
源码分析put(K key, V value)方法:
public V put(K key, V value) {
//static final Entry<?,?>[] EMPTY_TABLE = {};
//transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
if (table == EMPTY_TABLE) {
//如果数组为空,进行延迟初始化,默认长度为16
inflateTable(threshold);
}
if (key == null)
//遍历table[0] Entry链表 ,寻找e.key==null的Entry或者没有找到遍历结束
//如果找到了e.key==null,就保存null值对应的原值oldValue,然后覆盖原值,并返回oldValue
//如果在table[0]Entry链表中没有找到就调用addEntry方法添加一个key为null的Entry
return putForNullKey(value);
//计算key对应的hashCode值
int hash = hash(key);
//h & (length-1);与操作根据key对应的hashCode值与数组长度减一的值进行逻辑与运算,得到元素存放在数组中索引i
int i = indexFor(hash, table.length);
//遍历数组中索引为i处的链表,看是否有相同key的键值对存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果有,则替换旧的value值,并将其返回
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
//在LinkedHashMap中有实现具体逻辑,用于实现元素排序
e.recordAccess(this);
return oldValue;
}
}
//modCount++代表修改次数
modCount++;
//添加元素
addEntry(hash, key, value, i);
return null;
}
源码分析addEntry(int hash, K key, V value, int bucketIndex)方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//1、size:判断当前个数(指的是hashmap中的元素个数,而不是数组上的元素个数)是否大于等于阈值,阈值为数组长度乘以加载因子,加载因子默认0.75
//2、当前存放是否发生哈希碰撞(即要存储的数组索引位置是否已经存在元素,存在才扩容)
//如果上面两个条件都满足,那么就扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,容量扩大两倍,并且把原来数组中的元素重新放到新数组中
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//未扩容,map中添加新的元素
createEntry(hash, key, value, bucketIndex);
}
源码分析resize(int newCapacity) 方法:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//不满足扩容条件
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//新建数组
Entry[] newTable = new Entry[newCapacity];
//数组元素转移
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
源码分析transfer(Entry[] newTable, boolean rehash) 方法:
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;
//1. 默认:当新数组长度大于Integer最大值,才会重新hash算出转移元素在新数组上的下标
//2. 如果手动配置虚拟机jdk.map.althashing.threshold的环境变量,即Holder.ALTERNATIVE_HASHING_THRESHOLD得值,则可以调整hash种子hashSeed的值,让hash算法算出来更散列一点
//参照下面initHashSeedAsNeeded(int capacity)方法中的
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//没有重新计算hash值,用之前的hash值和新的数组长度重新计算的索引i要么等于之前数据的索引,要么等于之前数组的索引加上之前数组的长度。也就是将之前某一索引位置的长链表拆分为两个短链表
//比如:之前数组长度为4,之前元素索引为3,那么扩容后,数组长度为8,元素存放的索引为3或者7
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
JDK1.7 与JDK1.8中的HashMap对比:
不同点
- 发生hash冲突时
① JDK7:发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,旧元素移动到链表中。由于不用遍历链表,这种插入方式的效率是更高的。
② JDK8:发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8且数组长度大于64,如果是则将链表转成红黑树。因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。- 扩容时
① JDK7:在扩容resize()过程中,采用单链表的头插入方式,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。 多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。
② JDK8:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。