这一天,程序员们终于想起「面试」支配的恐惧。
今天又复习了一下程序猿常用的HashMap,发现呀,HashMap的优化程度是非常滴高~但同时可读性也非常的差,特别是JDK1.8。这里打算简单的科普一下HashMap中的底层实现。
HashMap的实现在JDK1.7 和 JDK1.8 是不一样的,所以抛开JDK版本来讲HashMap是不严谨的,JDK1.8对HashMap的数据结构作出了优化(引入了红黑树),同时也解决了多线程扩容的死循环链表问题。
JDK1.7 HashMap
HashMap的数据结构如下图所示,由一个数组+链表组成。
基本组成:
l 基础结点
每个结点用Entry表示,key-value对应当前table桶里的键值
next指向当前链表中的key-value对
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
l 基础信息
如代码所示,分别为初始默认值,最大默认值,负载因子,扩容阀值
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
int threshold; // 扩容阀值
代码分析:
1.初始化,new HashMap() 只初始化基本信息,不做对象的新建,在put中实现
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;
init();
}
2.put流程图与代码
补充1:其中hash()方法会对key进行hash,并用^=方法主要是为了让hash更散列
补充2:indexFor()方法会对hash进行size()-1的&方法代替对table[i]的求余%
原因a.前期限制了size大小必须是2进制数
原因b.用key.hash() & table.size()-1 实际上就是对key.hash()的后4位进行保留
E.g. hash : 1010 1010 (任意随机树)
Size-1:0000 1111 (size = 16)
&
Result:0000 1010
补充3:addEntry()插入HashMap实际上是用头插法,当遇到了哈希冲突,加入链表时是插入到头部。(JDK1.8以后改成尾插法)
代码详见下面代码块put()
3.扩容
在同一瞬间会存在两个HashMap,一个新的一个旧的,并通过链表的指针关联,把旧的链表list同步到新链表中。但是在并发环境中由于是头插法,链表的顺序会改变,同时并发场景下next指针会指向头结点,导致扩容死循环链表。
解决并发死循环问题:1.升级到JDK1.8 2.尽量不扩容,设置默认容量和扩容因子。
代码详见下面代码块resize()/transfer()
代码块:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
// 根据key计算哈希值
int hash = hash(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++;// fast-fail,迭代时响应快速失败,还未添加元素就进行modCount++,将为后续留下很多隐患
addEntry(hash, key, value, i);// 添加元素,注意最后一个参数i是table数组的下标
return null;
}
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;
// 遍历旧数组得到每一个key再根据新数组的长度重新计算下标存进去,如果是一个链表,则链表中的每个键值对也都要重新hash计算索引
for (Entry<K,V> e : table) {
// 如果此slot上存在元素,则进行遍历,直到e==null,退出循环
while(null != e) {
Entry<K,V> next = e.next;
// 当前元素总是直接放在数组下标的slot上,而不是放在链表的最后,所以扩容后的链表和旧的链表顺序是相反的
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 把原来slot(槽)上的元素作为当前元素的下一个(即如果哈希冲突,将当前元素插入到之前元素的前面)
e.next = newTable[i];
// 新迁移过来的节点直接放置在slot(槽)位置上
newTable[i] = e;
e = next;
}
}
}
JDK1.8 HashMap
JDK1.8 / JDK1.7 异同点,1.8的实现优化:
(1)数据结构通过数组+链表+红黑树实现
(2)获取table[i]位置时简化了扰动函数
(3)链表插入方法改成尾插法/红黑树,不会出现死循环链表
(这个图很好,但下一秒就是我的了)
JDK1.7/1.8 ConcurrentHashMap
JDK1.7中采用Segment+HashEntry实现,实际上的做法就是在HashMap之上再套一个table[i]数组,称为Segment[i]。
Segment继承了ReentranceLock有着锁的功能。ConcurrentHashMap初始化的时候会给出默认的SegmentSize,HashEntrySize。
每次put的时候,根据key的hash,定位到segment[i]的位置,如果没有初始化则通过CAS进行赋值,否则执行带锁的Segment put操作。
每次size的时候,统计每个segment元素个数进行累加。
JDK1.8中放弃了Segment臃肿的设计,采用Node+CAS+Synchronized。
put的时候,根据key的hash在node数组中找到对应位置,如果未初始化,通过CAS插入。如果已经初始化了,则通过Synchronized锁住当前node,更新结点(链表/红黑树)。
size通过volatile类型的basecount实现,每次插入或删除的时候都会更新这个字段。
参考文献
https://www.cnblogs.com/yangyongjie/p/11015174.html
https://blog.youkuaiyun.com/qq_36520235/article/details/82417949
https://www.jianshu.com/p/e694f1e868ec