@eric1989
2016-04-26T10:11:55.000000Z
字数 2243
阅读 719
Hashmap是一个key,value对应的数据结构。而内部实现是通过一个Entry数组来进行实现的。首先我们来看下Entry的代码。
class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
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操作,就会以不同路径执行下面的代码。
void transfer(Entry[] newTable, boolean rehash)
{
int newCapacity = newTable.length;
for (Entry<K,V> e : table)
{
while(null != e)
{
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;
}
}
}
一开始假设在某一个节点在上存在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代码如下
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
如果key的hash值落在循环链表处,并且不在已经存在的key中,那么这里是以null作为标志判断的,就出现了死循环。