JDK7和JDK8中HashMap的数据结构以及线程不安全和无序

JDK7中HashMap实现

jdk7中HashMap的数据结构是数组+链表来实现的,底层维护着一个数组,每个数组项是一个Entry;

transient Entry[] table;
static class Entry implements Map.Entry {
        final K key;
        V value;
        Entry next;
        int hash;

数组中的Entry的位置是通过key的HashCode进行计算的:

final int hash(Object k) {
        int h = 0;
        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

之后通过IndexFor进行计算出来:

static int indexFor(int h, int length) {
        return h & (length-1);
    }

其实就是key的hash值对length取模;如果两个key的hash值一样,那么就产生冲突,或者说碰撞;HashMap解决碰撞的方法是通过链表。将新的值存放在entry[i]数组中,原来的值作为entry[i]的next;所以新插入的值放在链表的头节点中,旧值存放在链表的尾部;

当size大于capacity*装载因子的时候就发生扩容;

void addEntry(int hash, K key, V value, int bucketIndex) {
        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);
    }

每次扩容,容量变为原来的2倍;

JDK8中的HashMap实现

在jdk7中,如果上百个元素存在一个链表上,那么如果要查找其中的一个元素的时候,查找时间为o(n),性能比较低的;在Jdk8中解决了这个问题,通过引入红黑树,在最差的情况下,时间复杂度为o(logN);

JDK8中HashMap的实现为数组+链表/红黑树;默认当链表的长度大于8之后,数据结构就变成红黑树;如图所示

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第1张图片

jdk8中的定义如下

transient Node[] table;

节点名字不再是entry,而是node,就是因为和红黑树的实现TreeNode关联;put方法如下所示

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab;
	Node p; 
	int n, i;
	//如果当前map中无数据,执行resize方法。并且返回n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
	 //如果要插入的键值对要存放的这个位置刚好没有元素,那么把他封装成Node对象,放在这个位置上就完事了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
	//否则的话,说明这上面有元素
        else {
            Node e; K k;
	    //如果这个元素的key与要插入的一样,那么就替换一下,也完事。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
	    //1.如果当前节点是TreeNode类型的数据,执行putTreeVal方法
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
		//还是遍历这条链子上的数据,跟jdk7没什么区别
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
			//2.完成了操作后多做了一件事情,判断,并且可能执行treeifyBin方法
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null) //true || --
                    e.value = value;
		   //3.
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
	//判断阈值,决定是否扩容
        if (++size > threshold)
            resize();
	    //4.
        afterNodeInsertion(evict);
        return null;
    }
红黑树

红黑树是二叉排序树,但在每个结点增加了一个存储位来标识颜色,red或者black;满足二叉排序树的所有特性:

1.若任意结点的左子树不为空,则左子树的所有结点的值小于根结点;

2.若任意结点的右子树不为空,则右子树的所有结点的值大于根结点;

3.左右子树也是二叉排序树;

4.没有键值相等的节点

一个有n个节点的二叉排序树的高度为lgn,所以查找时间复杂度为O(lgn);

线程不安全的

HashMap是线程不安全的,是因为在resize的时候会产生死循环;默认的size是16,当超过这个size之后,会扩容,这样一来,整个Hash表中的元素都需要被重新计算一遍,实现代码如下所示:

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
 
        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
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);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
}

transfer简单解释下:每次取出旧数组的头结点的next,之后重新计算头结点在新的Hash中的位置,然后将头节点的next指向新的table[i],然后把table[i]设置成当前的头结点,那么就完成了头结点的转移;

所以转移之后的顺序会跟之前的顺序相反,比如原来是1->2->3,转移之后为

第一次    1

第二次    2    1

第三次    3    2    1,

顺序正好反过来了。HashMap的死锁问题就出在这个transfer函数上;

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第2张图片

用个图进行简单的说明:

线程一执行到这里被挂起;线程二执行了

Entry next = e.next;

完成了3和7元素的转移之后,线程一就接着执行,这时候,线程一种3.next是7,线程二中7.next是3;就形成了环形的链表;

另外:在迭代的过程中,如果有线程修改了map,会抛出ConcurrentModificationException错误,就是所谓的fail-fast策略;

无序的

HashMap是无序的,先通过一个例子验证下

 HashMap< String, String> map = new HashMap<>();
        map.put("5", "@sohu.com");
        map.put("2","@163.com");
        map.put("3", "@sina.com");
        for (String key : map.keySet()) {
            System.out.println("key= "+key+" and value= "+map.get(key));
        }

程序的返回结果:

key= 2 and value= @163.com
key= 3 and value= @sina.com
key= 5 and value= @sohu.com

可以发现,放进去的顺序和遍历的时候的顺序是不一致的;那么为什么会是这样的那,我们需要先了解下HashMap的遍历方式;

先说map.keySet的方式,keySet的代码如下所示(Jdk8的情况)

  public Set keySet() {
        Set ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

可以看到是new了一个keySet对象,这个KeySet是一个内部类,代码如下所示

final class KeySet extends AbstractSet {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.this.clear(); }
        public final Iterator iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

这个内部类中的iterator方法实现了迭代器接口,接口定义代码如下

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第3张图片

这里有一点需要说明下,增强for循环,那么什么是增强for循环那,上面遍历keyset的for就是一个增强for循环:

for(String key:keys)类似这样的,底层在迭代的时候还是使用的迭代器,当然这个调用是jvm来完成的,我们只需要知道调用for的时候就会调用iterator方法,我们再来接着看iteraotr方法返回一个new keyIterator对象

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第4张图片

然后KeyIteraotr类的定义如下所示;

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第5张图片

这里的nextNode方法如下所示

JDK7和JDK8中HashMap的数据结构以及线程不安全和无序_第6张图片

这里看到的就是最终的核心了,真正的遍历过程;之前我们说HashMap的数据结构是数组+链表,这里遍历的时候从第一个元素开始,然后遍历链表,当链表遍历结束的时候,遍历下面一个数组和对应的链表数据;

现在再来看看HashMap为啥是无序的,因为存放的时候是根据key的Hash值来存放的,先放进去的计算hash之后可能存放在数组的后面了,所以遍历之后就在后面的再遍历出来;



你可能感兴趣的:(数据结构)