HashMap简析

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


存储结构

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

transient Entry[] 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 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 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 e : table) {
            while(null != e) {
                Entry 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]的链表上将产生死循环。

你可能感兴趣的:(源码分析)