HashMap1.7采用数组+链表(头插法)的方式
相关属性
capacity 数组的大小
loadFactor 负载因子
threshold 阈值
size 容器中kv键值对的大小
hashSeed哈希种子
modeCount对hashmap结构发生调整的次数,并非修改某个kv值的次数
DEFAULT_INITIAL_CAPACITY默认初始容量 16 1<<4
MAXIMUM_CAPACITY最大容量 2的30次方 1<<30
DEFAULT_LOAD_FACTOR默认负载因子0.75f
map链表上每个节点的数据结构
源码解析
构造函数
HashMap()–>调用HashMap(DEFAULT_INITIAL_CAPACITY[16], DEFAULT_LOAD_FACTOR[0.75f])
HashMap(int initialCapacity)->调用HashMap(initialCapacity, DEFAULT_LOAD_FACTOR[0.75f])
HashMap(int initialCapactiy, float loadFactor)
HashMap(Map对象)
以上构造函数可以只看HashMap(int initialCapactiy, float loadFactor)和HashMap(Map对象)
HashMap(int initialCapactiy, float loadFactor)
ps:此时只是初始化相关参数,但没有初始化数组,只是默认table是空数组
public HashMap(int initialCapacity, float loadFactor) {
// 判断初始容量和负载因子的有效性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
// 不可超过最大的容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// 一开始的阈值=传入的初始容量
threshold = initialCapacity;
// 相关初始化方法,hashMap是空实现,让子类自己的实现
init();
}
HashMap(Map对象)
public HashMap(Map<? extends K, ? extends V> m) {
// 计算初始化容量initialCapactiy= max(m.size / 0.75f +1, 16)
// 负载因子取0.75f
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 根据传入的值进行map属性的调整
// 计算实际的capacity和threshold,并创建table数组
inflateTable(threshold);
// 将m的数据迁移到新创建的table数组
putAllForCreate(m);
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
// 找到大于等于toSize的最小二次幂值
// 传12,capacity为16
int capacity = roundUpToPowerOf2(toSize);
// threshold阈值与capacity紧密关联,所以capacity一改变,threshold得重新计算
// threshold = min(16*0.75, 2^30 +1) = 12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 创建新数组
table = new Entry[capacity];
// capacity一改变,计算新的hashSeed哈希种子,让能够更加均匀
// initHashSeedAsNeeded会返回是否调整了哈希种子
initHashSeedAsNeeded(capacity);
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
// 遍历旧map的所有k-v,调用putForCreate方法不停迁移数据
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putForCreate(e.getKey(), e.getValue());
}
private void putForCreate(K key, V value) {
// 空key直接查到table[0]链表
int hash = null == key ? 0 : hash(key);
// 根据hash值和数组的大小capacity进行取余运算
// index = hash % capacity
// 因为当capacity是2的整数幂 2^n -->取余运算可转化为 index = hash & (capacity - 1) 按位与运算更快
int i = indexFor(hash, table.length);
/**
* Look for preexisting entry for key. This will never happen for
* clone or deserialize. It will only happen for construction if the
* input Map is a sorted map whose ordering is inconsistent w/ equals.
*/
// 获取到对应的链表,不停遍历当前链表,判断遍历的节点hash是否一致,且key是否一致,一致则覆盖旧值
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
e.value = value;
return;
}
}
// 当前链表找不到旧节点,就创建新节点
createEntry(hash, key, value, i);
}
/**
* 当前方法不用考虑map扩容,毕竟先初始好了
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 新建节点,并用头插法插入链表的头部
table[bucketIndex] = new Entry<>(hash, key, value, e);
// kv大小自增
size++;
}
put方法
public V put(K key, V value) {
// 每次put,得判断数组已经被初始化
// new HashMap的时候,table取EMPTY_TABLE默认值
if (table == EMPTY_TABLE) {
// table若为默认值,则进行初始化
// threshold一开始传入的initialCapacity,计算capacity为大于等于threshold的2次幂,threshold、创建table数组,hash种子
inflateTable(threshold);
}
if (key == null)
// 直接插入table[0]位置
return putForNullKey(value);
// 计算hash-->key得到hashCode再进行9次干扰运算
int hash = hash(key);
// 得到key应该放置的链表
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 从链表上找到key一致的节点,覆盖旧值,并返回旧值
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 没有找到旧记录,就新增节点,结构改变,modCount++
modCount++;
// addEntry考虑扩容情况,而createEntry不考虑扩容
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
// key为null,hash值统一为0,那么index=0
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 没有找到旧记录,就新增节点,结构改变,modCount++
modCount++;
// addEntry考虑扩容情况,而createEntry不考虑扩容
addEntry(0, null, value, 0);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 判断是否扩容
// 条件1:当前kv的数量是否大于等于阈值
// 条件2:要插入的节点是不是空链表上
// 个人理解: 阈值是数组大小*负载因子,如果大于等于阈值,证明数量已经分布在多个槽位,容易发生hash冲突。但是如果要放在空链表,则不会哈希冲突,可以忍受
if ((size >= threshold) && (null != table[bucketIndex])) {
// 每次扩容的长度都是原来的两倍
resize(2 * table.length);
// 扩容完成则需要计算新的hash值和数组索引
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
// 直接创建节点,不用考虑扩容
createEntry(hash, key, value, bucketIndex);
}
resize方法-扩容
void resize(int newCapacity) {
// newCapacity是旧数组的两倍
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
// 如果旧数组的大小因此达到最大值,不用扩容了,只是改变阈值的大小为2^31-1
threshold = Integer.MAX_VALUE;
return;
}
// 创建新数组,更新hash种子,返回是否重新计算hash值
Entry[] newTable = new Entry[newCapacity];
// 迁移旧数组上的数据到新数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 覆盖原数组
table = newTable;
// 计算阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// transfer方法是造成多线程并发情况下,死循环的原因
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历数组上的所有链表
for (Entry<K,V> e : table) {
// 遍历链表上所有节点,从头到尾一个个采用头插法插到新链表
// 会造成顺序倒置,原本A->B->C->NULL,变成C->B->A->NULL
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;
// 遍历旧链表下个节点,直到null
e = next;
}
}
}
remove方法
public V remove(Object key) {
// 根据key找到旧节点,剔除并返回对应的value
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
// 先判断size大小
if (size == 0) {
// 空,就不用处理了
return null;
}
// 计算hash值和对应的数组索引
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
// 通过prev和e指针去遍历链表
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
// 找到移除的节点,modCount++
modCount++;
size--;
// e是第一个节点
if (prev == e)
table[i] = next;
else
// e是大于1的节点
prev.next = next;
// 找到节点并删除,返回结果
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
// 找不到,返回null
return e;
}
clear方法
public void clear() {
modCount++;
// 将table的每个值都置为null,让jvm回收掉之前的数据,毕竟不在gc-root引用链上
Arrays.fill(table, null);
// size置为0
size = 0;
// 当时threshold、数组大小啥的还在
}
负载因子0.75
值太高,则链表已经非常长了,哈希冲突的概率很大,很浪费查询时间
值太低,则哈希冲突小,链表短,虽然查询效率提升,但是空间浪费很严重。
0.75是为了平衡时间和空间。
多线程不安全问题-死循环
HashMap的所有方法和属性都没有设置锁的相关操作,是线程不安全的。当在高并发情况下,多个线程同时put,且已经达到扩容条件时,容易出现链环的问题,继而导致get方法出现死循环问题。原因就出现扩容方法resize的transfer操作。
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);
}
/**
* Transfers all entries from current table to newTable.
*/
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;
}
}
}
- 线程1和线程2同时进入resize方法,创建两个新数组。
- 线程1赋值e=A,next=e.next=B。切换到线程2
- 线程2完成所有操作,链上的节点为C=>B=>A=>NUll,见图①。切换到线程1
- 线程1此时e=A,next=B。将A插入新链表。没什么问题。见图②。
- 线程1再循环一次,e=B,next=A。将B头插法插入新链表。见图③。
- 线程1再循环一次,e=A,next=null。将A插入新链表,见图4.此时链上已经生成闭环A->B->A
- 线程1此时e=null,退出循环。完成扩容操作,结果为图⑤。
- 当get方法去获取那个闭环链表,如果key不一致,会导致死循环,cpu狂飙。