精进之路之HashMap

HashMap本质的核心就是“数组+链表”,数组对于访问速度很快,而链表的优点在于插入速度快,HashMap集两者于一身。html

提到HashMap,咱们不得不提各个版本对于HashMap的不一样。本文中先从1.6版本谈起,分别从结构,hash,扩容等几方面展开来看。在具体讨论以前,咱们先了解下HashMap的结构:java

JDK1.6之结构:
算法

 从图中咱们能够看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。咱们来看看java代码:数组

 

/**
* The table, resized as necessary. Length MUST Always be a power of two.
* 表,根据须要调整大小。长度必须是2的幂
*/
transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //当前的key
V value;//当前的value
Entry<K,V> next;//下一个元素
final int hash;// hash值

/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
......
}

上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。        当咱们往hashmap中put元素的时候,先根据key的hash值获得这个元素在数组中的位置(即下标),而后就能够把这个元素放到对应的位置中了。
若是这个元素所在的位子上已经存放有其余元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最早加入的放在链尾。
从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,而后经过key的equals方法在对应位置的链表中找到须要的元素。
从这里咱们能够想象获得,若是每一个位置上的链表只有一个元素,那么hashmap的get效率将是最高的。

JDK1.6之hash算法:
咱们能够看到在hashmap中要找到某个元素,须要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。
前面说过hashmap的数据结构是数组和链表的结合,因此咱们固然但愿这个hashmap里面的元素位置尽可能的分布均匀些,尽可能使得每一个位置上的元素数量只有一个,
那么当咱们用hash算法求得这个位置的时候,立刻就能够知道对应位置的元素就是咱们要的,而不用再去遍历链表。
因此咱们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来讲是比较均匀的。
可是,“模”运算的消耗仍是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样作的,
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1);
}


首先算得key得hashcode值,而后跟数组的长度-1作一次“与”运算(&)。看上去很简单,其实比较有玄机。
好比数组的长度是2的4次方,那么hashcode就会和2的4次方-1作“与”运算。
不少人都有这个疑问,为何hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高
我以2的4次方举例,来解释一下为何数组大小为2的幂时hashmap访问的性能最高。         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,可是很明显,当它们和1110“与”的时候,
产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就须要遍历这个链表,
获得8或者9,这样就下降了查询的效率。同时,咱们也能够发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,
那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费至关大,
更糟的是这种状况中,数组可使用的位置比数组长度小了不少,这意味着进一步增长了碰撞的概率,减慢了查询的效率!


 因此说,当数组长度为2的n次幂的时候,不一样的key算得得index相同的概率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的概率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
          说到这里,咱们再回头看一下hashmap中默认的数组大小是多少,查看源代码能够得知是16,为何是16,而不是15,也不是20呢,看到上面annegu的解释以后咱们就清楚了吧,显然是由于16是2的整数次幂的缘由,在小数据量的状况下16比15和20更能减小key之间的碰撞,而加快查询的效率。

因此,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码以下(HashMap的构造方法中):
数据结构

// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;


JDK1.6之resize(默认扩充为原来的两倍):
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
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);//转换新表
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}

/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable) {
Entry[] src = table;//扩容前
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {//进行老entry遍历
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
       当hashmap中的元素愈来愈多的时候,碰撞的概率也就愈来愈高(由于数组的长度是固定的),
因此为了提升查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操做也会出如今ArrayList中,
因此这是一个通用的操做,不少人对它的性能表示过怀疑,不过想一想咱们的“均摊”原理,就释然了,
而在hashmap数组扩容以后,最消耗性能的点就出现了:原数组中的数据必须从新计算其在新数组中的位置,并放进去,这就是resize。          那么hashmap何时进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,
也就是说,默认状况下,数组大小为16,那么当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,而后从新计算每一个元素在数组中的位置,
而这是一个很是消耗性能的操做,因此若是咱们已经预知hashmap中元素的个数,那么预设元素的个数可以有效的提升hashmap的性能。好比说,咱们有1000个元素new HashMap(1000), 可是理论上来说new HashMap(1024)更合适,不过上面annegu已经说过,即便是1000,hashmap也自动会将其设置为1024。 可是new HashMap(1024)还不是更合适的,由于0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 咱们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。

1.7多线程

 
  • 加入了jdk.map.althashing.threshold这个jdk的参数用来控制是否在扩容时使用String类型的新hash算法。
  • 把1.6的构造方法中对表的初始化挪到了put方法中。
  • 1.6中的tranfer方法对旧表的节点进行置null操做(存在多线程问题),1.7中去掉了。
 

1.8less

 

hashmap有了重大更新,其内部实现采用了红黑树,entry链表长度超过阈值8,就会转为树结构,性能有了较大提高。性能

 

ConcurrentHashMap一样进行了巨大更新,放弃使用以前的分区锁,而是使用CAS原子操做来提供修改树节点的原子操做,其锁的粒度实际是节点,this

故性能比之前有了很多的提高。和hashmap同样采用树结构,可是树的根节点是不同的,也就是数组节点不同。spa

 

注意: resize 发生在大于等于临界值,而不仅仅是大于临界值,如下代码为例:当前size先进性了自增1操做,故size=threshold 时,便会发生resize()

 

 

 注:本文摘自、整理以下文章,感谢原做者的倾心分享:http://www.iteye.com/topic/539465

推荐相关文章:http://www.importnew.com/28263.html

相关文章
相关标签/搜索