先说HashMap最重要的一点:缺点
HashMap的缺点我们大都听说过,其在高并发的情况下表现较差,会出现一些奇奇怪怪的问题,比如使CPU使用率提高到100%(此处打个小差,因为前几天,我的服务器莫名其妙CPU占用率也达到了100%,我还以为是跑了哪个项目写的有问题了,后来查了一下所有进程才发现有个ddgs的一直在高占用,经过研究发现,这是一个新型的挖矿病毒,中毒原因是我之前练习redis的时候忘了设置密码o(╥﹏╥)o),那么它为什么会出现这个原因呢?其实这个高并发下的问题,也和HashMap一个长久以来的缺点相挂钩,没错,就是HashMap 的扩容机制。
为什么说HashMap的扩容机制是长久以来的缺点,我们可以简单看一下其源码,可知:其初始化长度为16,扩容因子为0.75(即当内容达到百分之七十五的时候会扩容为当前的二倍),那么问题其实就在于HashMap是怎样进行扩容的。
它在扩容的时候,会先生成一个新的HashMap,然后把原HashMap里的数据一个一个的复制到新的HashMap里,那么就很轻易的知道了,当我们的数据量过大的时候,我们的HashMap会进行多次扩容,那么就会相对来说很消耗我们的资源,解决这个缺点的办法也很简单,先大致预估一下我们需要在这个HashMap里存放多少数据,然后在初始化它的时候给它先把默认长度给设置了,这样就避免了多次扩容多次复制。
介绍完了扩容机制,那么其在高并发下那个100%的问题是怎么来的呢,不难想出,上述步骤中,最有可能出现问题的,就是复制的那一步。
//扩容的方法
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];
//在此进行我们所说的复制的那一步 传入的参数为新HashMap, 初始化hash掩码(此处不太懂也没事)
transfer(newTable, initHashSeedAsNeeded(newCapacity));
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循环是我们问题的关键
while(null != e) {
/*当我们遍历到一个不为空的老数据的时候,我们假定这个老数据在横向(我们知
道HashMap是由数组和链表构成的,那么假设一个二维空间里HashMap纵向是数组结构,横向是链表结构)挂载这有下一个节点,
那么我们现在想移动这个老数据,必须得保证我们下边节点的数据不丢失,所以我们创建一个next先去"指向"它*/
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;
}
}
}
在这个transfer方法中,当我们看到
Entry<K,V> next = e.next;
的时候,我们就应该有这样一个担忧,在高并发的情况下,这一步会不会影响我们的扩容,答案是肯定的……
但是说这是不是一个BUG ? 并不
sun公司的负责人表达的意思是:我们设计HashMap本来的作用就不应该是应对高并发情况的,在高并发的情况下,我们有另外一个更好用的ConcurrentHashMap 去应对。
HashMap的put方法注意点
我们通常会简单认为HashMap的put方法的Key只进行一次hash运算,但事实上,HashMap的put实现是在计算key的hash之上,又进行了一次自己规定的位运算,以JDK1.8中源码为例(版本有差距,不过也都是进行了二次位运算)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h; //^异或运算 >>>为带符号右移
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}