Jdk1.7下HashMap常用方法的源码分析

1.首先是从hashmap中put元素开始分析

 HashMap<String, String> map = new HashMap<>();
 map.put("123", "1");
 map.get("123");

2.点击put方法进入到里面

public V put(K key, V value) {
	    // 初始化动作
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
	   // 插入key为null的判断
        if (key == null)
            return putForNullKey(value);
	    // 计算key在hashmap中数组的对应的下标
        int hash = hash(key);
        int i = indexFor(hash, table.length);
	    // 遍历map找到和当前key相等的,并覆盖原来的值,返回旧值
        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++;
	    // 增加一个entry
        addEntry(hash, key, value, i);
        return null;   
}

3.点击addEntry进入到里面去

void addEntry(int hash, K key, V value, int bucketIndex) {
	// 判断实际大小是否超过了阈值,并且数组上对应下标的值不为null,则扩容为原来2倍
	 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);
}

4.先进入到继续添加entry的方法中来,点击createEntry

void createEntry(int hash, K key, V value, int bucketIndex) {
	// e的引用指向对应数组下标上的值
    Entry<K,V> e = table[bucketIndex];
    // 创建一个新的entry,然后对应的next指向上面那个数组下标的值,最后将数组的引用改为新创建的entry的引用,实现了头插法,并向下移动了元素
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // hashmap的实际容量加1
    size++;
}

对应的效果图如下,其中假设数组长度为4,对应下标为2,在单线程下移动的步骤如下:

第一步,e的引用指向对应数组下标上的值:
在这里插入图片描述

第二步,创建一个新的entry,然后对应的next指向上面那个数组下标的值:

在这里插入图片描述

第三步,将数组的引用改为新创建的entry的引用,实现了头插法,并向下移动了元素

在这里插入图片描述

5.我们再回来看hashmap中的resize方法

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
    table = newTable;
    // 重新计算阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    
}

6.继续点击transfer方法,进入到里面来

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 遍历数组
    for (Entry<K,V> e : table) {
    	// 这里假设指向数组下标为2的元素
        while(null != e) {
        	// 然后指向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;
        }
    }
}

上面这个代码的逻辑是,扩容之后转移元素到新的数组上,下面是在单线程下的情况扩容

第一步,遍历数组,指向数组上元素,然后next指向e的下一个

在这里插入图片描述
第二步,扩容数组长度为6,重新计算hash之后,对应数组下标有两种情况一种是还是之前位置,另外一种是原来位置的长度加上原来数组的长度,例如,之前下标是2,扩容之后下标为2或者5

下面这个图对应e.next = newTable[i];这一段代码逻辑

在这里插入图片描述
第三步,头插法插入元素,并下移元素,对应newTable[i] = e;这一段代码逻辑

在这里插入图片描述
第四步,e重新指向next元素,对应e = next;这一段代码逻辑

在这里插入图片描述
第五步,以此类推,完成了对原数组的扩容之后的转移,头插法之后元素的顺序会颠倒到来

在这里插入图片描述
7.而在多线程情况下就会出现循环链表,这里假设一个线程已经完成了元素转移,但是另外一个线程在只执行到transfer方法的newTable[i] = e;这一段代码的时候被挂起了,在它重新获得cpu执行时间片的时候,再进行转移就会出现循环链表,这里是由于jdk1.7下put的时候采用的是头插法,转移之后会对原有的元素顺序反转,所以就会出现循环链表

在这里插入图片描述

8.并且在多线程下,还会出现数据丢失问题,同样是在上面那种情况下一个执行完成,另外一个挂起之后再执行,就会出现下面图的情况,转移之后原来的数据3丢失了

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值