2-2-3-9-1-1、jdk1.7HashMap详解


数据结构

jdk1.7的hashmap的底层结构是数组加单向链表实现的。将key的hash值进行取模获取index既即将存放的元素的数组的位置。然后到对应的链表中进行put和get操作

链表的作用

因为对数组进行取模的时候可能会遇到获取index的位置是一样的,所以可能会遇到hash碰撞冲突,而HashMap使用链表来存储hash碰撞的元素

链表问题

如果在hash碰撞发生严重的情况下,则会出现插入和获取元素时间过长的问题,jdk1.8对此进行了树化来解决这个问题,详情见jdk1.8HashMap的详解

数据结构简图

在这里插入图片描述


数组(绿色):hash数组(桶),数组元素是每个链表的头节点
链表(浅蓝色):解决hash冲突,不同的key映射到了数组的同一索引处,则形成链表

源码解析

重要成员变量说明

//默认初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认负载因子  超过 容量*负载因子  就需要扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认空数组
static final Entry<?,?>[] EMPTY_TABLE = {};

//用来存放元素得数组,默认== EMPTY_TABLE 用来判断hashMap是否真正的初始化
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//元素数量
transient int size;

//构造函数时为设置的容量,初始化后为容量*负载因子
int threshold;

//负载因子
final float loadFactor;

为什么负载因子的大小默认为0.75而不是1呢
这涉及到一个数学运算
根据HashMap的扩容机制,他会保证capacity的值永远都是2的幂
那么,为了保证负载因子(loadFactor) * 容量(capacity)的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的幂乘积结果都是整数
如果我们把负载因子设置成1,容量使用默认初始值16,那么表示一个HashMap需要在"满了"之后才会进行扩容,显然会发生更多的hash碰撞
总结:负载因子默认为0.75是最适合减少hash碰撞又能保证 loadFactor*capacity的结果是一个整数的一个值

构造函数

public HashMap() {
    //调用下面的构造函数  16, 0.75F
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//                容量                    负载因子
public HashMap(int initialCapacity, float loadFactor) {
    //容量不能小于0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    //容量最大为MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    //设置当前负载因子为默认负载因子
    this.loadFactor = loadFactor;
    //构造函数时threshold 为 容量
    threshold = initialCapacity;
    //空方法
    init();
}

put操作

public V put(K key, V value) {
    //如果table == EMPTY_TABLE则table没有真正的初始化,需要初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }

    //hashmap 可以存放null key
    if (key == null)
        return putForNullKey(value);

    //计算key的hash
    int hash = hash(key);

    //求出当前key要存放在数组中的下标
    int i = indexFor(hash, table.length);

    //遍历当前下标的key, 看有没有相同的key, 如果有就替换value,返回oldValue
    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++;

    //说明当前位置没有相同的key,新增Entry放入链表
    addEntry(hash, key, value, i);
    //存放前key不存在 返回null作为oldValue
    return null;
}

初始化数组

private void inflateTable(int toSize) {
    //hashMap为了方便计算下标中的位置,所以容量需要设置为2的次方,但是没有做强制校验
    //如果设置的容量不是2的次方,这里会获取到比容量大 且 最小的2的次方
    int capacity = roundUpToPowerOf2(toSize);

    //获取扩容的门槛,容量 * 负载因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

    //初始化数组为2的次方的capacity
    table = new Entry[capacity];

    //为了看一下capacity是否需要重置一下哈希种子,是为了得到的哈希值更加散列。
    initHashSeedAsNeeded(capacity);
}

Key为null的处理

private V putForNullKey(V value) {
    //如果key==null,在1.7中就会把该元素放入下标为0的位置
    //如果0位置已经存在元素,就需要判断是否是同一个key,如果是,就需要替换oldValue并返回
    //不是null的元素求得的位置也可能是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++;

    //此时key==null,这一步表示0位置没有key==null的Entry,新增Entry入链表
    addEntry(0, null, value, 0);
    return null;
}

计算hash

 final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

 static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

添加链表节点–新增Entry

void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断是否扩容 
    //1.当前size >= capacity * loadFactory
    //2.当前下标位置不等于null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容,容量必须是2的次方,所以扩容是旧容量的2倍
        resize(2 * table.length);
        //重新计算在扩容之后hash
        hash = (null != key) ? hash(key) : 0;
        //重新计算在扩容之后的下标
        bucketIndex = indexFor(hash, table.length);
    }

    //放入Entry
    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    //获取到当下下标的entry
    Entry<K,V> e = table[bucketIndex];
    //采用头插法放入链表头部,e是当前下标的元素,下面Entry的构造函数时next
    //即把当前下标的e作为新元素的next,把next放到当前下标位置
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

扩容
void resize(int newCapacity) {
    //临时保存旧table
    Entry[] oldTable = table;
    //临时保存旧table的长度
    int oldCapacity = oldTable.length;
    //如果旧容量已经达到了最大容量,不扩容
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    //创建扩容后的数组
    Entry[] newTable = new Entry[newCapacity];

    //把旧数据拷贝到新数组,  initHashSeedAsNeeded(newCapacity)重置新的数组hash种子返回是否需要rehash
    transfer(newTable, initHashSeedAsNeeded(newCapacity));

    //替换旧数组
    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) {
        //只有当前的e != null 才需要拷贝
        while(null != e) {
            //临时保存一下 next,这点也是出现回环链表 死循环的原因
            Entry<K,V> next = e.next;

            //是否重新计算hash
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }

            //计算在新数组中的位置
            int i = indexFor(e.hash, newCapacity);
            //其实还是头插法,如果当前位置已经有元素了,就把该元素作为e的next
            e.next = newTable[i];
            //把e放入该位置
            newTable[i] = e;
            //把扩容前的e的next复制给e继续执行该流程,完成旧数组链表的下一个元素的转移
            e = next;
        }
    }
}

缺点

HashMap是线程不安全的,不安全的具体原因就是在高并发场景下,扩容可能产生死锁 (Jdk1.7存在)以及get操作可能带来的数据丢失

扩容死锁分析

死锁问题核心在于下面代码,多线程扩容导致形成的链表环! 以下代码是去除了与分析死锁无关的代码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            //记录oldhash表中e.next 
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //rehash计算出数组的位置(hash表中桶的位置) 
            int i = indexFor(e.hash, newCapacity);
            //e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个元素
            e.next = newTable[i];
            //将e放入到new hash表的头部
            newTable[i] = e;
            //转移e到下一个节点, 继续循环下去
            e = next;
        }
    }
}

单线程扩容

**假设:**hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上
**扩容:**hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞,这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上(5%4=3), 当前e.next为null, 跳出while循环,resize结束
过程如下图所示:

在这里插入图片描述

多线程扩容

下面就是多线程同时put的情况了, 然后同时进入transfer方法中:假设这里有两个线程同时执行了put()操作,并进入了transfer()环节

while(null != e) {
    //线程1执行到此被调度挂起(cpu时间片轮转等发生上下文切换)
    //记录oldhash表中e.next
    Entry<K,V> next = e.next;
    //rehash计算出数组的位置(hash表中桶的位置) 
    int i = indexFor(e.hash, newCapacity);
     //e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个元素
    e.next = newTable[i];
    //将e放入到new hash表的头部
    newTable[i] = e;
    //转移e到下一个节点, 继续循环下去
    e = next;
}

那么此时的状态为:

在这里插入图片描述


从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表
然后线程1被唤醒了:

  1. 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null
  2. 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash表的 key(3)。好了,e 处理完毕
  3. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

  1. 现在的 e 节点是 key(7),首先执行Entry next = e.next,那么 next 就是key(3)了
  2. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)

此时的状态为:

在这里插入图片描述


然后又该执行 key(7)的 next 节点 key(3)了:

  1. 现在的 e 节点是 key(3),首先执行Entry next = e.next,那么 next 就是null
  2. 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:

在这里插入图片描述


很明显,环形链表出现了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值