JDK7 Hashmap源码解析及头插法的缺点

JDK7版本中Hashmap主要构成为:数组+链表
结构布局如图所示:
在这里插入图片描述
HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个红色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
下面展示一些 内联代码片

//hashmap源码
 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
        .......
    }

hashmap在初始化时的重要的几个参数:

  1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
  2. loadFactor:负载因子,默认为 0.75
  3. threshold:扩容的阈值,等于 capacity * loadFactor

插入元素

主要分为两步,如插入<K1,V1>值:

  1. 计算K1的hashCode,通过hashCode找到桶下标,可通过取余法,源码中是通过:&运算(hashcode&(数组长度-1))
  2. 根据桶下标,如果为NULL,直接将元素插入,如冲突,即可采用链表法解决冲突(源码中采用头插法进行插入),在插入时,会遍历整个链表,查看是否存在相同K的键值对,如存在,就更新value.

插入时NULL键默认插入数组中第一个。
整个源码如下:
下面展示一些 内联代码片

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        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 = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

在插入过程中,上面代码中的 addEntry(hash, key, value, i),内部原理如下:
下面展示一些 内联代码片

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);
    }

但插入键值对的总数量size>=threshhold(这个参数初始化时说过),就需要对数组进行扩容处理,扩为2倍。并且对旧数组Entity元素进行遍历,转移到新数组中。

// 创建新数组,并且重新金酸
 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];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

元素转移。

  // 数组元素转移
    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;
            }
        }
    }

jdk8中 最大区别是数据结构采用的是(数组+链表+红黑树),当链表长度大于8时,改为红黑树,并且链表的插入采用的是尾插法。
其他元素操作,可详看源码,
JDK7的hashmap是线程不安全的,这里列出两个常见问题,进行解释:

HashMap死循环问题

JDK8以后没有这个问题。
以下图解进行解释:
我们插入<1,1>,<2,2>, < 3,3>这三个元素,数组大小以2.

在这里插入图片描述
我们进行扩容为4.但多线程并发执行时,结果如下:
在这里插入图片描述
线程二在完成过程中(假设线程二在获取原数组的e和next先于线程一),线程一先完成,如上图,此时线程二拿到的当前e和next在线程一中,此时线程二进行数据转移时,会出现如下情况

具体步骤代码参考上面transfer()源码在这里插入图片描述

元素缺失问题

同上诉道理,可以解释元素缺失问题。
具体可看:https://www.cnblogs.com/dongguacai/p/559[HashMap线程安全问题]

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值