JDK7版本中Hashmap主要构成为:数组+链表
结构布局如图所示:
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个红色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
下面展示一些 内联代码片
。
//hashmap源码
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
.......
}
hashmap在初始化时的重要的几个参数:
- capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
- loadFactor:负载因子,默认为 0.75
- threshold:扩容的阈值,等于 capacity * loadFactor
插入元素
主要分为两步,如插入<K1,V1>值:
- 计算K1的hashCode,通过hashCode找到桶下标,可通过取余法,源码中是通过:&运算(hashcode&(数组长度-1))
- 根据桶下标,如果为NULL,直接将元素插入,如冲突,即可采用链表法解决冲突(源码中采用头插法进行插入),在插入时,会遍历整个链表,查看是否存在相同K的键值对,如存在,就更新value.
插入时NULL键默认插入数组中第一个。
整个源码如下:
下面展示一些 内联代码片
。
public V put(K key, V value) {
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;
}
在插入过程中,上面代码中的 addEntry(hash, key, value, i),内部原理如下:
下面展示一些 内联代码片
。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
但插入键值对的总数量size>=threshhold(这个参数初始化时说过),就需要对数组进行扩容处理,扩为2倍。并且对旧数组Entity元素进行遍历,转移到新数组中。
// 创建新数组,并且重新金酸
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];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
transfer(newTable, rehash);
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
元素转移。
// 数组元素转移
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;
}
}
}
jdk8中 最大区别是数据结构采用的是(数组+链表+红黑树),当链表长度大于8时,改为红黑树,并且链表的插入采用的是尾插法。
其他元素操作,可详看源码,
JDK7的hashmap是线程不安全的,这里列出两个常见问题,进行解释:
HashMap死循环问题
JDK8以后没有这个问题。
以下图解进行解释:
我们插入<1,1>,<2,2>, < 3,3>这三个元素,数组大小以2.
我们进行扩容为4.但多线程并发执行时,结果如下:
线程二在完成过程中(假设线程二在获取原数组的e和next先于线程一),线程一先完成,如上图,此时线程二拿到的当前e和next在线程一中,此时线程二进行数据转移时,会出现如下情况
具体步骤代码参考上面transfer()源码
元素缺失问题
同上诉道理,可以解释元素缺失问题。
具体可看:https://www.cnblogs.com/dongguacai/p/559[HashMap线程安全问题]