HashMap简单源码及多线程下的死循环

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap简单源码及多线程下的死循环相关的知识,希望对你有一定的参考价值。

主要记录hashMap的一些基本操作源码实现原理以及多线程情况下get()操作的死循环引发原因

一、hashMap简介

1.hashMap集合的主要属性及方法

(默认初始化容量)DEFAULT_INITIAL_CAPACITY = 16

(默认最大容量)MAXIMUM_CAPACITY = 1 << 30

(默认加载因子)DEFAULT_LOAD_FACTOR = 0.75f

(Entry数组)Entry[] table

(Entry实例的数量)size

 put(K key, V value)方法

 get(K key)方法

2.hashMap结构及操作(new方法 put方法 get方法):

数组+链表的形式,以实例Entry<K,V>的形式存储

技术分享

a.new方法

从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组(默认长度16,加载因子0.75)

源码:

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }

例:table[0],table[1],table[2],table[3]...table[15]

 

b.put方法

 当执行put方法时,会先计算key值的hash值,将hash值和(数组长度减一得到的值)进行与运算,得到数组下标值,将键值对以Entry实例的形式放入数组中:

 put方法源码:

    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode()); //计算key的hash值
        int i = indexFor(hash, table.length); //根据hash值和数组长度计算数组位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如遇到hash冲突(e不为空),遍历链表
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如遇到key值相等的,进行替换
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
indexFor方法源码(计算数组下标),方法和简单,就把key值的hash值和数组长度-1做了与运算
    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

例:添加键值对为7,77,调用方法put(7,77),7经过计算后的hash值为7(可自行验证,不多做解释),调用indexFor方法进行与运算:0111&1111=0111,下标值为7。

table[0],table[1],table[2],table[3]...table[7]=7...table[15]

再添加元素8,put(8,88),15经过计算后的hash值仍为8,1000&1111=1000,下标值为8。

table[0],table[1],table[2],table[3]...table[7]=7,table[8]=8...table[15]

再添加元素22,put(22,2222),22经过计算后的hash值为23,10111&1111=7,下标值为7,此时由于table[7]中已存在元素(7,77),产生hash冲突,hashMap会将23放入table[7]中,再执行(e=e.next),以链表形式将next指针指向之前的元素(7,77),如下图所示:

技术分享

当Entry的个数超过最大容量值*负载因子(16*0.75=12)时,hashMap会进入resize方法,重新创建一个数组并扩容为原来的2倍,再将数据拷贝到新的数组中,如下图所示:

技术分享

resize方法源码:

    /**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    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);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

c.get方法:

当执行get方法时,根据key值的hash值,得到数组下标,将数组内的entry的key值与get中的参数做对比(如该数组内有链表,则会继续遍历链表),若hash相等且equals,则返回。

get方法源码:

   /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it‘s also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode()); //计算hash值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
//这里遍历数组中的链表,若找到key值相等的,则返回
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }


 二、Java多线程下的HashMap死循环

摘自:http://blog.csdn.net/xiaohui127/article/details/11928865

正常的ReHash的过程

画了个图做了个演示。

  • 我假设了我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
  • 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
  • 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程
  • 技术分享

     

并发下的Rehash

1)假设我们有两个线程。我用红色和浅蓝色标注了一下。

我们再回头看一下我们的 transfer(resize方法中)代码中的这个细节:

 

                do {
                    Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了  
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);

 

而我们的线程二执行完成了。于是我们有下面的这个样子。

技术分享

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。

2)线程一被调度回来执行。

  • 先是执行 newTalbe[i] = e;
  • 然后是e = next,导致了e指向了key(7),
  • 而下一次循环的next = e.next导致了next指向了key(3)

技术分享

3)一切安好。

线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

技术分享

4)环形链接出现。

e.next = newTable[i] 导致  key(3).next 指向了 key(7)

注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

技术分享

于是,当我们的线程一调用到,HashTable.get(11)时,悲剧就出现了——Infinite Loop。

 



以上是关于HashMap简单源码及多线程下的死循环的主要内容,如果未能解决你的问题,请参考以下文章

多线程下HashMap的死循环问题

深入理解JAVA集合系列三:HashMap的死循环解读

深入理解JAVA集合系列三:HashMap的死循环解读

老生常谈,HashMap的死循环

JDK1.7源码分析集合HashMap的死循环

Jdk1.7下的HashMap源码分析