HashMap源码阅读笔记

Hashmap源码阅读笔记


概要原理介绍

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(n)。
在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作为标志判断的,就出现了死循环。

猜你喜欢

转载自blog.csdn.net/kuangzhanshatian/article/details/47946387