[关闭]
@eric1989 2016-04-26T10:11:55.000000Z 字数 2243 阅读 719

Hashmap源码阅读笔记


概要原理介绍

Hashmap是一个key,value对应的数据结构。而内部实现是通过一个Entry数组来进行实现的。首先我们来看下Entry的代码。

  1. class Entry<K,V> implements Map.Entry<K,V> {
  2. final K key;
  3. V value;
  4. Entry<K,V> next;
  5. int hash;

Entry对象由四个元素构成。如果能够通过key找到对应的entry,也就能找到value。所以hashmap的get方法时间复杂度为O(1)。
在hashmap中进行读写操作时,首先都会通过key来进行位置的定位,而这里为了加快定位,不是采用key的hash值进行mod数组长度这种方式确定。而是通过更为快速的并运算得到。

计算散列值并且分散到数组下标

在放入键值对时需要计算key的hashcode并且将对应的hashcode放入到某一个数组下标。这里不采用mod方法,因为数字计算耗时。
这里采用一个更为简单的方式。由于数组的长度Length都是2的倍数,所以实际上数组的下标到Length-1为止。此时将key的hashcode和length-1进行bing操作,即可得到一个数字,该数字即可成为hashcode在数组的下标值。
简单一点说,比如数组长度为8,那么实际上最大的下标在内存中的位表示是00001111.用这样的数字进行并运算, 得到的结果必然是小于length的,也就是在数组长度之内的下标。

扩容

当hashmap中的Entry数组的大部分内容都被填充后,由于接下来key发生碰撞的几率很高,此时应该进行扩容操作。扩容操作在单线程中很简单,就是单纯的将数组2倍增大,并且将原来的元素重新执行hash计算放入到新的数组中即可。

碰撞

所谓碰撞,就是说不同key的hash值计算并且求得散列到下标的结果是一直的。这里注意,同一个下标的hashcode是有可能不一致的。
当碰撞发生的时候,将新的entry放入对应的entry数组的所在位置,然后将next指针指向原来的数组对应位置,也就是说每次插入,都在头结点进行。这样可以提高插入的效率。

多线程并发出现死循环

hashmap设计是为了给单线程环境使用的。如果在多线程环境中使用,就很容易出现死循环的情况。下面来分析下如何出现这一情况。
在多线程并发进行put操作的时候,就都可能同时执行扩容操作。扩容操作就是生成一个新的2倍的数组,然后将元素迁移到新数组中。在迁移的过程中需要进行重新hash计算和放入。
具体的代码如下。而如果两个线程同时进行相同大小的resize操作,就会以不同路径执行下面的代码。

  1. void transfer(Entry[] newTable, boolean rehash)
  2. {
  3. int newCapacity = newTable.length;
  4. for (Entry<K,V> e : table)
  5. {
  6. while(null != e)
  7. {
  8. Entry<K,V> next = e.next;
  9. if (rehash)
  10. {
  11. e.hash = null == e.key ? 0 : hash(e.key);
  12. }
  13. int i = indexFor(e.hash, newCapacity);
  14. e.next = newTable[i];
  15. newTable[i] = e;
  16. e = next;
  17. }
  18. }
  19. }

死循环开始

一开始假设在某一个节点在上存在entry链表1,2,3,null。在resize之后,这3个元素仍然是在一起。那么执行完resize操作后,顺序应该会变成3,2,1,null。现在线程1和线程2都开始执行相同的resize操作。
在执行完成Entry next = e.next;这步后,线程1进入等待。线程2则全部执行完毕。那么此时的状态应该是3,2,1,null。此时线程1恢复执行。
此时线程1的执行情况如下面的表格所示。

执行语句 执行结果
初始情况 e=1,next=2;2.next=1,1.next=null
e.next = newTable[i];newTable[i] = e;e = next; newTable[i] =1,e=2;1.next=null,2.next=1
Entry next = e.next;e.next = newTable[i];newTable[i] = e;e = next; e=1,newTable[i]=2;2.next=1,1.next=null
Entry next = e.next;e.next = newTable[i];newTable[i] = e;e = next; newTable[i]=1,e=null;1.next=2,2.next=1

到这里,在该链表处,就形成了循环。然后resize方法是正常退出的。
而在这之后,调用get方法时就会出席死循环。get代码如下

  1. final Entry<K,V> getEntry(Object key) {
  2. if (size == 0) {
  3. return null;
  4. }
  5. int hash = (key == null) ? 0 : hash(key);
  6. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  7. e != null;
  8. e = e.next) {
  9. Object k;
  10. if (e.hash == hash &&
  11. ((k = e.key) == key || (key != null && key.equals(k))))
  12. return e;
  13. }
  14. return null;

如果key的hash值落在循环链表处,并且不在已经存在的key中,那么这里是以null作为标志判断的,就出现了死循环。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注