HashMap实现原理
基本组成
HashMap 由Entry数组组成,Entry下是链表(jdk1.8变成红黑树)。
HashMap是基于hashing的原理,当我们给put()方法传递键和值时,我们先对键调用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象。
如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
put过程:
- 先判断key是不是为空,为空做插入操作;
- 对key做hash操作,跟据hash结果定位数组下标,判断该下标的entry是否存在,如果存在,遍历链表查找key值相同的entry,并更新其value值,并返回旧值;如果不存在执行第3步新增entry操作;
- 新增时,判断数据(已插入entry的数量)长度是否大于等于12=16*0.75,如果是则resize操作。
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
创建entry,插入链头
你了解重新调整HashMap大小存在什么问题吗?
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。
在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。
如果条件竞争发生了,那么就死循环了。(同时调整理大小导致链环)
我们看下过程:
Entry的结构为:
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
resize过程:
当 ((size >= threshold) 才resize
其中
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
即:threshold=16*0.75=12时resize
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
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);
}
void transfer(Entry[] newTable) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;//第一个线程执行到停止,第二个线程执行完后,第一个线程接着执行
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//此处理发生循环:原table为3->7 ,e=3 当第二个线程执行完后,newTable[i]=7->3 第一个线程执行此行时, 3 ->(7->3) 此时产生环
newTable[i] = e;
e = next;
}
}
}
并发执行时,从entry头开始拷贝时
原:entry head 3 ->7 tail
新:entry head 7 ->3 tail