HashMap简析

来源:互联网 发布:unity3d选中物体高亮 编辑:程序博客网 时间:2024/06/13 10:57

一次电话面试中,面试官询问,HashMap是线程不安全的,那么并发使用时造成死循环的原因是什么。可惜由于我本来知道它是线程不安全的,从未写过并发读取的代码,因此没有遇到过这个问题,只好回答没遇到过。身为java程序员,没有研究过HashMap源码也确实说不过去,遂在面试结束后仔细阅读了JDK7的HashMap.java源码,简单分析如下文。


存储结构

所有数据存储在Entry的数组中,但是注意,并不是一个元素对应数组中的一个Entry,数组中的一个Entry代表一系列序列(indexFor方法计算)相同的键值对,使用Entry的next属性关联成链表,具体可以看get和put方法如何寻找HashMap是否已经包含了key:

transient Entry<K,V>[] table;

示意图如下,方框为Entry数组(或者称其为table),数组中每一个元素可能是一个链表:




增加元素

HashMap中put方法先寻找是否已经包含了该key,如果包含,覆盖原来对应的value,并返回旧的value,,否则创建一个新的Entry。

public V put(K key, V value) {// key的index(hash与table.length-1进行与运算)值相同的元素,用Entry链表保存,因此这里要进行循环next,直到找到该key        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;// 将e的value进行覆盖,并返回oldValue                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;// 需要put进来的key在HashMap中不存在,则创建一个新的Entry        addEntry(hash, key, value, i);        return null;    }void addEntry(int hash, K key, V value, int bucketIndex) {        // addEntry首先判断是否超出threshold,如果超出,则进行resize,否则直接createEntry        if ((size >= threshold) && (null != table[bucketIndex])) {            resize(2 * table.length);            hash = (null != key) ? hash(key) : 0;            bucketIndex = indexFor(hash, table.length);        }        createEntry(hash, key, value, bucketIndex);    }void createEntry(int hash, K key, V value, int bucketIndex) {        Entry<K,V> e = table[bucketIndex];// table[bucketIndex]用新的Entry对象覆盖,Entry的构造方法中将原table[bucketIndex]对象赋值给新Entry的next,形成链表。也就是一直在链表头部新增元素。        table[bucketIndex] = new Entry<>(hash, key, value, e);        size++;    }


线程安全

众所周知,HashMap不是线程安全的,那么并发时会出现什么问题呢。首先各种计数器会计算错误,并且同时操作table[i]会造成数据丢失或覆盖。除此以外,由于存储数据采用链表,且put和get中都会对链表进行循环,那么会不会产生闭环链表,导致死循环出现呢。
上面addEntry方法中在数组容量不足时,会进行调用resize方法扩容,该方法重新创建了新的Entry数组,即table变量,并且将老的table中对象使用transfer方法转移到新的table中。

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);// 这里将老的table的数据,转移到新Table中,每个table[i]的链表都进行循环,将newTable[i]赋值给当前Entry对象的next,并将newTable[i]指向当前对象,即一直在链表头部增加原table链表中的next,转移完成后,链表相当于倒转了。                e.next = newTable[i];                newTable[i] = e;                e = next;            }        }    }

这个方法很有意思,例如在table[i]中存储了1-->2-->3-->4这样一个链表,transfer方法相当于翻跟头一样,将数据复制到newTable中: 每次循环后newTable[i]的链表变化为:step1:1    step2 :2-->1    step3:3-->2-->1    step4: 4-->3-->2-->1,想象下如果transfer方法同时有两个线程进入,一前一后操作同一个table[i],可能出现这样的情况:

线程一在step3中将newTable[i]赋值为3-->2-->1,而线程二刚走到step2,将newTable[i]更新为2-->3-->2-->1,闭环的链表出现了,后面无论是get还是put操作,在newTable[i]的链表上将产生死循环。

0 0
原创粉丝点击