HashMap原理分析-3(JDK1.7 & 对比1.8)

HashMap原理分析-3(JDK1.7 & 对比1.8)

提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!


HashMap原理分析-3(JDK1.7)

JDK7多线程情况,HashMap扩容可能会出现循环链表问题:
解决多线程出现的问题:

  1. 如果HashMap的长度提前就能决定,可以初始化的时候就指定足够的长度,防止扩容
  2. 在上层调用HashMap put()方法的位置加锁
  3. 使用HashTable(效率比较低)
  4. 使用ConcurrentHashMap(分段加锁)

源码分析put(K key, V value)方法:

public V put(K key, V value) {
		//static final Entry<?,?>[] EMPTY_TABLE = {};
		//transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
        if (table == EMPTY_TABLE) {
        	//如果数组为空,进行延迟初始化,默认长度为16
            inflateTable(threshold);
        }
        if (key == null)
        	//遍历table[0] Entry链表 ,寻找e.key==null的Entry或者没有找到遍历结束
			//如果找到了e.key==null,就保存null值对应的原值oldValue,然后覆盖原值,并返回oldValue
			//如果在table[0]Entry链表中没有找到就调用addEntry方法添加一个key为null的Entry
            return putForNullKey(value);
        //计算key对应的hashCode值
        int hash = hash(key);
        //h & (length-1);与操作根据key对应的hashCode值与数组长度减一的值进行逻辑与运算,得到元素存放在数组中索引i
        int i = indexFor(hash, table.length);
        //遍历数组中索引为i处的链表,看是否有相同key的键值对存在
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //如果有,则替换旧的value值,并将其返回
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                //在LinkedHashMap中有实现具体逻辑,用于实现元素排序
                e.recordAccess(this);
                return oldValue;
            }
        }
        //modCount++代表修改次数
        modCount++;
        //添加元素
        addEntry(hash, key, value, i);
        return null;
    }

源码分析addEntry(int hash, K key, V value, int bucketIndex)方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
		//1、size:判断当前个数(指的是hashmap中的元素个数,而不是数组上的元素个数)是否大于等于阈值,阈值为数组长度乘以加载因子,加载因子默认0.75
    	//2、当前存放是否发生哈希碰撞(即要存储的数组索引位置是否已经存在元素,存在才扩容)
    	//如果上面两个条件都满足,那么就扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	//扩容,容量扩大两倍,并且把原来数组中的元素重新放到新数组中
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//未扩容,map中添加新的元素
        createEntry(hash, key, value, bucketIndex);
    }

源码分析resize(int newCapacity) 方法:

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

源码分析transfer(Entry[] newTable, boolean rehash) 方法:

      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;
                
                 //1. 默认:当新数组长度大于Integer最大值,才会重新hash算出转移元素在新数组上的下标
                 //2. 如果手动配置虚拟机jdk.map.althashing.threshold的环境变量,即Holder.ALTERNATIVE_HASHING_THRESHOLD得值,则可以调整hash种子hashSeed的值,让hash算法算出来更散列一点
                 //参照下面initHashSeedAsNeeded(int capacity)方法中的
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                
                //没有重新计算hash值,用之前的hash值和新的数组长度重新计算的索引i要么等于之前数据的索引,要么等于之前数组的索引加上之前数组的长度。也就是将之前某一索引位置的长链表拆分为两个短链表
                //比如:之前数组长度为4,之前元素索引为3,那么扩容后,数组长度为8,元素存放的索引为3或者7
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

JDK1.7 与JDK1.8中的HashMap对比:

面试阿里,HashMap 这一篇就够了

不同点

  1. 发生hash冲突时
    ① JDK7:发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,旧元素移动到链表中。由于不用遍历链表,这种插入方式的效率是更高的。
    ② JDK8:发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8且数组长度大于64,如果是则将链表转成红黑树。因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。
  2. 扩容时
    ① JDK7:在扩容resize()过程中,采用单链表的头插入方式,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。 多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。
    ② JDK8:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。

HashMap与ConcurrentHashMap对比:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值