jdk7和jdk8的实现是不一样的,jdk7采用数组+链表实现,jdk8采用数组+链表+红黑树实现。
HashMap线程不安全,有线程安全需求的要用ConcurrentHashMap替代。
HashMap允许key为null,不允许key重复。
HashMap并发下put()会导致死链,导致cpu打满的原因不是死链的形成,而是查询时死链会导致无限循环。
jdk7版
主要成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化大小 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子0.75
static final Entry<?,?>[] EMPTY_TABLE = {}; // 初始化的默认数组
transient int size; // HashMap中元素的数量
int threshold; // 域值,每次扩容要重新计算下,用来判断是否需要调整HashMap的容量
Entry类型定义
// 在HashMap里的静态内部类 ,Entry用来存储键值对,HashMap中的Entry[]用来存储entry
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //键
V value; //值
Entry<K,V> next; //采用链表存储HashCode相同的键值对,next指向下一个entry
int hash; //entry的hash值
//构造方法, 负责初始化entry
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//当使用相同的key的value被覆盖时调用
void recordAccess(HashMap<K,V> m) {
}
//每移除一个entry就被调用一次
void recordRemoval(HashMap<K,V> m) {
}
}
到这里就可以给出HashMap的抽象结构图了:
关于这种结构,我们还要了解的是存储对象是如何分布到数组和链表上的,数组是如何扩容的,搞完这两点,基本就没啥问题了。
以上这两个关键点,我们应该能从put()函数中得到解答
// 向map中添加key-value 键值对,如果可以包含了key的映射,则旧的value将被替换
public V put(K key, V value) {
if (table == EMPTY_TABLE) { // table如果为空,进行初始化操作
inflateTable(threshold);
}
if (key == null) // key 为null ,放入数组的0号索引位置
return putForNullKey(value);
int hash = hash(key); // 计算key的hash值
int i = indexFor(hash, table.length); // 计算key在entry数组中存储的位置
// 判断该位置是否已经有元素存在
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 判断key是否已经在map中存在,若存在用新的value替换掉旧的value,并返回旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); // 空方法
return oldValue;
}
}
modCount++; // 修改次数加1
addEntry(hash, key, value, i); // 将key-value转化为Entry,添加到Map中,扩容操作在里面
return null;
}
// 扩充表,HashMap初始化时是一个空数组,此方法创建一个新的Entry[]
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize,power是幂的意思
int capacity = roundUpToPowerOf2(toSize); // capacity为2的幂数,大于等于toSize
// 计算阈值,用来确定需不需要扩容
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity]; // 新建数组,并重新赋值
initHashSeedAsNeeded(capacity); // 修改hashSeed
}
// 根据hashcode,和表的长度,返回存放的索引,按位与的效果
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
// 添加实体,bucketIndex是数组下标,关键点是扩容
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);
}
// 扩容操作,并发下可能形成死链,打满cpu
void resize(int newCapacity) {
Entry[] oldTable = table; // 将table赋值给新的引用
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个长度为newCapacity的数组
Entry[] newTable = new Entry[newCapacity];
// 将table中的元素复制到newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
// 更改阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 将table中的数据复制到newTable中,死链形成
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
// 遍历原table
for (Entry<K,V> e : table) {
while(null != e) {
// 遍历桶(链表)
Entry<K,V> next = e.next; // 这个和下面的e = next配合形成一个循环
if (rehash) { // 是否需要重新计算Hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
// 重新确定所属数组角标
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; // 让被迁移的e指向头,首次为null
newTable[i] = e; // 替换头,e是共享变量
e = next; // 迭代下一个
}
}
}
迁移链表时并发下是如何产生死链的?如果多个线程同时进行扩容,肯定会形成多个新的数组+链表结构,但是迁移的数据还是原来的,在堆上并没有变化,只是被多个新的Hash结构引用了,假设线程1刚执行了next=e.next,就被挂起,线程2开始迁移并完成一个桶的迁移,然后线程1被唤醒,
此时线程1看到的结构如下,其实已经被迁移了,但是它还是会操作
这里我们看到,经过3轮循环后,迁移结束了,但是环形链表形成了,下次查找时就会形成死循环,导致cpu某一个核心被打满,其中也能看到,并发resize()也可能会导致数据丢失,总之就是结果完全无法预知。
jdk8版本
jdk8中HashMap引入了红黑树,原因是碰撞频繁使,链表长度过长导致查询变慢,所以jdk8中,当链表的长度达到一定值(默认是8)时,将链表转换成红黑树(时间复杂度为O(lg n)),极大的提高了查询效率。
主要成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
transient Node<K,V>[] table;
transient int size;
可以看到,数组结构的类型已经变成了Node<>类型。Node<>结构定义如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
看看jdk8中HashMap大致结构:
再来看看扩容是怎么做的:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}