JDK1.7-HashMap-源码分析以及死循环问题分析

HashMap1.7采用数组+链表(头插法)的方式

相关属性

capacity 数组的大小

loadFactor 负载因子

threshold 阈值

size 容器中kv键值对的大小

hashSeed哈希种子

modeCount对hashmap结构发生调整的次数,并非修改某个kv值的次数

DEFAULT_INITIAL_CAPACITY默认初始容量 16 1<<4

MAXIMUM_CAPACITY最大容量 2的30次方 1<<30

DEFAULT_LOAD_FACTOR默认负载因子0.75f

在这里插入图片描述

map链表上每个节点的数据结构

在这里插入图片描述

源码解析

构造函数

HashMap()–>调用HashMap(DEFAULT_INITIAL_CAPACITY[16], DEFAULT_LOAD_FACTOR[0.75f])

HashMap(int initialCapacity)->调用HashMap(initialCapacity, DEFAULT_LOAD_FACTOR[0.75f])

HashMap(int initialCapactiy, float loadFactor)

HashMap(Map对象)

以上构造函数可以只看HashMap(int initialCapactiy, float loadFactor)和HashMap(Map对象)

HashMap(int initialCapactiy, float loadFactor)

ps:此时只是初始化相关参数,但没有初始化数组,只是默认table是空数组

public HashMap(int initialCapacity, float loadFactor) {
    // 判断初始容量和负载因子的有效性
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        // 不可超过最大的容量
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
		
    this.loadFactor = loadFactor;
    // 一开始的阈值=传入的初始容量
  
    threshold = initialCapacity;
    // 相关初始化方法,hashMap是空实现,让子类自己的实现
    init();
    
}
HashMap(Map对象)
public HashMap(Map<? extends K, ? extends V> m) {
    // 计算初始化容量initialCapactiy= max(m.size / 0.75f +1, 16)
    // 负载因子取0.75f
    this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                  DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
    // 根据传入的值进行map属性的调整
    // 计算实际的capacity和threshold,并创建table数组
    inflateTable(threshold);
	// 将m的数据迁移到新创建的table数组
    putAllForCreate(m);
}
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
   	// 找到大于等于toSize的最小二次幂值
    // 传12,capacity为16
    int capacity = roundUpToPowerOf2(toSize);
	// threshold阈值与capacity紧密关联,所以capacity一改变,threshold得重新计算
    // threshold = min(16*0.75, 2^30 +1) = 12
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 创建新数组
    table = new Entry[capacity];
    // capacity一改变,计算新的hashSeed哈希种子,让能够更加均匀
    // initHashSeedAsNeeded会返回是否调整了哈希种子
    initHashSeedAsNeeded(capacity);
}
private void putAllForCreate(Map<? extends K, ? extends V> m) {
    // 遍历旧map的所有k-v,调用putForCreate方法不停迁移数据
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putForCreate(e.getKey(), e.getValue());
}

private void putForCreate(K key, V value) {
    // 空key直接查到table[0]链表
    int hash = null == key ? 0 : hash(key);
    // 根据hash值和数组的大小capacity进行取余运算
    // index = hash % capacity
    // 因为当capacity是2的整数幂 2^n -->取余运算可转化为 index = hash & (capacity - 1) 按位与运算更快
    int i = indexFor(hash, table.length);

    /**
         * Look for preexisting entry for key.  This will never happen for
         * clone or deserialize.  It will only happen for construction if the
         * input Map is a sorted map whose ordering is inconsistent w/ equals.
         */
    // 获取到对应的链表,不停遍历当前链表,判断遍历的节点hash是否一致,且key是否一致,一致则覆盖旧值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            e.value = value;
            return;
        }
    }
	// 当前链表找不到旧节点,就创建新节点
    createEntry(hash, key, value, i);
}

/**
* 当前方法不用考虑map扩容,毕竟先初始好了
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 新建节点,并用头插法插入链表的头部
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // kv大小自增
    size++;
}

put方法

public V put(K key, V value) {
    // 每次put,得判断数组已经被初始化
    // new HashMap的时候,table取EMPTY_TABLE默认值
    if (table == EMPTY_TABLE) {
        // table若为默认值,则进行初始化
        // threshold一开始传入的initialCapacity,计算capacity为大于等于threshold的2次幂,threshold、创建table数组,hash种子
        inflateTable(threshold);
    }
    if (key == null)
        // 直接插入table[0]位置
        return putForNullKey(value);
    // 计算hash-->key得到hashCode再进行9次干扰运算
    int hash = hash(key);
    // 得到key应该放置的链表
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 从链表上找到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++
    modCount++;
    // addEntry考虑扩容情况,而createEntry不考虑扩容
    addEntry(hash, key, value, i);
    return null;
}


private V putForNullKey(V value) {
	// key为null,hash值统一为0,那么index=0
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 没有找到旧记录,就新增节点,结构改变,modCount++
    modCount++;
    // addEntry考虑扩容情况,而createEntry不考虑扩容
    addEntry(0, null, value, 0);
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 判断是否扩容
    // 条件1:当前kv的数量是否大于等于阈值
    // 条件2:要插入的节点是不是空链表上
    // 个人理解: 阈值是数组大小*负载因子,如果大于等于阈值,证明数量已经分布在多个槽位,容易发生hash冲突。但是如果要放在空链表,则不会哈希冲突,可以忍受
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 每次扩容的长度都是原来的两倍
        resize(2 * table.length);
       	// 扩容完成则需要计算新的hash值和数组索引
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
	// 直接创建节点,不用考虑扩容
    createEntry(hash, key, value, bucketIndex);
}

resize方法-扩容

void resize(int newCapacity) {
    // newCapacity是旧数组的两倍
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // 如果旧数组的大小因此达到最大值,不用扩容了,只是改变阈值的大小为2^31-1
        threshold = Integer.MAX_VALUE;
        return;
    }
	// 创建新数组,更新hash种子,返回是否重新计算hash值
    Entry[] newTable = new Entry[newCapacity];
    // 迁移旧数组上的数据到新数组
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 覆盖原数组
    table = newTable;
    // 计算阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

// transfer方法是造成多线程并发情况下,死循环的原因
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    // 遍历数组上的所有链表
    for (Entry<K,V> e : table) {
        // 遍历链表上所有节点,从头到尾一个个采用头插法插到新链表
        // 会造成顺序倒置,原本A->B->C->NULL,变成C->B->A->NULL
        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;
            // 遍历旧链表下个节点,直到null
            e = next;
        }
    }
}

remove方法

public V remove(Object key) {
    // 根据key找到旧节点,剔除并返回对应的value
    Entry<K,V> e = removeEntryForKey(key);
    return (e == null ? null : e.value);
}

final Entry<K,V> removeEntryForKey(Object key) {
    // 先判断size大小
    if (size == 0) {
        // 空,就不用处理了
        return null;
    }
    // 计算hash值和对应的数组索引
    int hash = (key == null) ? 0 : hash(key);
    int i = indexFor(hash, table.length);
    // 通过prev和e指针去遍历链表
    Entry<K,V> prev = table[i];
    Entry<K,V> e = prev;

    while (e != null) {
        Entry<K,V> next = e.next;
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k)))) {
            // 找到移除的节点,modCount++
            modCount++;
            size--;
            // e是第一个节点
            if (prev == e)
                table[i] = next;
            else
               // e是大于1的节点
                prev.next = next;
            // 找到节点并删除,返回结果
            e.recordRemoval(this);
            return e;
        }
        prev = e;
        e = next;
    }
	// 找不到,返回null
    return e;
}

clear方法

public void clear() {
    modCount++;
    // 将table的每个值都置为null,让jvm回收掉之前的数据,毕竟不在gc-root引用链上
    Arrays.fill(table, null);
    // size置为0
    size = 0;
    // 当时threshold、数组大小啥的还在
}

负载因子0.75

值太高,则链表已经非常长了,哈希冲突的概率很大,很浪费查询时间

值太低,则哈希冲突小,链表短,虽然查询效率提升,但是空间浪费很严重。

0.75是为了平衡时间和空间。

多线程不安全问题-死循环

​ HashMap的所有方法和属性都没有设置锁的相关操作,是线程不安全的。当在高并发情况下,多个线程同时put,且已经达到扩容条件时,容易出现链环的问题,继而导致get方法出现死循环问题。原因就出现扩容方法resize的transfer操作。

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

/**
     * Transfers all entries from current table to newTable.
     */
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;
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WB0nRv9t-1647144279586)(C:\Users\Stone\AppData\Roaming\Typora\typora-user-images\image-20220313115832626.png)]

  1. 线程1和线程2同时进入resize方法,创建两个新数组。
  2. 线程1赋值e=A,next=e.next=B。切换到线程2
  3. 线程2完成所有操作,链上的节点为C=>B=>A=>NUll,见图①。切换到线程1
  4. 线程1此时e=A,next=B。将A插入新链表。没什么问题。见图②。
  5. 线程1再循环一次,e=B,next=A。将B头插法插入新链表。见图③。
  6. 线程1再循环一次,e=A,next=null。将A插入新链表,见图4.此时链上已经生成闭环A->B->A
  7. 线程1此时e=null,退出循环。完成扩容操作,结果为图⑤。
  8. 当get方法去获取那个闭环链表,如果key不一致,会导致死循环,cpu狂飙。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值