编程语言
首页 > 编程语言> > JDk1.7 HashMap源码解析——线程安全问题

JDk1.7 HashMap源码解析——线程安全问题

作者:互联网

Jdk1.7的HashMap, 在多线程环境下,扩容的时候可能会形成环状链表导致死循环和数据丢失问题。

HashMap在扩容的流程

  1. 扩容相关常量
/**
`* 默认负载因子
`*/`
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
`* Entry数组
`*/`
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
 * 容量, 默认达到 size * 0.75 就会扩容
 */
transient int size;
  1. 扩容的条件

当我们新增元素(put 方法)的时候,需要去判断是否能够存下这个元素,如果存的下就存,存不下就扩容再存。

  1. 扩容流程

以 put 方法 为例

public V put(K key, V value) {
    // 判断是否是空表
    if (table == EMPTY_TABLE) {
        // 初始化, 强制把 初始化容量 转换为 2 的整次幂
        inflateTable(threshold);
    }
    // 判断是否是空值
    if (key == null)
        return putForNullKey(value);
    // 获取 hash 值
    int hash = hash(key);
    // 获取 索引
    int i = indexFor(hash, table.length);
    // 遍历 指定索引下的链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断put 的key是否已经存在
        // 如果存在,则替换
        // 并返回 旧值
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 如果 put 的key不存在,则添加进去
    // 此方法会判断是否要扩容
    addEntry(hash, key, value, i);
    return null;
}

addEntry 方法, 会判断数组当前容量是否已经超过的阈值,例如,假设当前的数组容量是16,加载因子为0.75,即超过了12,并且刚好要插入的索引处有元素,这时候就需要进行扩容操作,可以看到resize扩容大小是原数组的两倍,仍然符合数组的长度是2的指数次幂。

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 判断是否需要扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容
        resize(2 * table.length);
        // 重新计算hash值
        hash = (null != key) ? hash(key) : 0;
        // 计算所要插入的桶的索引值
        bucketIndex = indexFor(hash, table.length);
    }
    // 新增Entry
    createEntry(hash, key, value, bucketIndex);
}

resize 方法, 首先,如果这个HashMap的容量已经非常大了,新的长度会大于我们预设的最大容量,这时直接return;来终止这个方法。如果没有,后面会进行数组的转移操作,即transfer方法。

initHashSeedAsNeeded方法, 主要是判断一下是否需要初始化散列,尽量避免HashMap的值太过集中不够散列。

这里 预设的最大容量是 MAXIMUM_CAPACITY = 1 << 30, 就是 MAXIMUM_CAPACITY = 2 ^ 30, 如果达到最大 预设容量, 那么,HashMap 可以使用的 的容量就是 Integer.MAX_VALUE

/**
 * 扩容
 */
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 达到最大值,无法扩容
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // 设置为 HashMap 的最大容量
        threshold = Integer.MAX_VALUE;
        return;
    }
    Entry[] newTable = new Entry[newCapacity];
    // 将数据转移到新的Entry[]数组中
    // 新数组是旧数组的两倍大小
    transfer(newTable, initHashSeedAsNeeded(newCapacity));//初始化散列种子
    // 覆盖原数组
    table = newTable;
    // 重新计算 可以使用的容量
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer 方法, 把原来数组的值逐一复制到这个新的数组, 首先是遍历table数组,如果遍历到Entry不为空,我们进入while循环,进行链表操作,每次操作结束都将进入循环的e用e.next覆盖,直至链表到达尾部。

/**
 * 
 * @param newTable 新数组的引用
 * @param rehash true代表需要重新获取 hash 值
 */
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;
        }
    }
}

多线程下,链表的插入

这里 模拟了两个线程 A 和 B,在并发情况下,插入数据的扩容图示:

注意: 图中所有的箭头都是指针(或者引用),线程 A 的新数组 是 数组A;线程 B 的新数组 是 数组B。

假设,线程 A 运行到

Entry<K,V> next = e.next;

的代码时阻塞了, 因为线程 A 被阻塞了,其后面的代码就没法继续执行了,而此时线程 B 也进入方法进行扩容,扩容后的结果就是单线程时扩容后的结果,此时相比于扩容前的HashMap,原数组的链表元素的位置已经调换。

线程 B 的图示 是就是简单的单线程扩容,就只画出执行后的结果图;

线程 A 的图示 依赖于 线程 B 的结果,每个图示,代表一次 while 循环后的结果。

图示:

在这里插入图片描述

可以看到,最后出现了环,并且 数据 8 丢失了。

标签:扩容,hash,HashMap,JDk1.7,数组,源码,key,table,Entry
来源: https://blog.csdn.net/weixin_44730681/article/details/112055560