HashMap笔记整合

HashMap(1.7之前?)底层由数组+链表实现

如果发生哈希冲突时,插入链表时使用头插法,因为一般认为后插入的元素被查找的可能性更大
HashMap的初始长度为16,且每次自动扩容或手动指定初始化长度必须为2的整数次幂
这是因为计算做key的hash运算时能更好的平均分布
公式为:index = HashCode(Key) & (Length - 1)length为HashMap长度 &运算:全为1为1,其余为0
例:HashCode为1001,若长度为16,二进制位1111,做与运算为1001,结果为9
HashCode为1101,若长度为16,二进制位1111,做与运算为1101,结果为13
结果完全取决于HashCode的值
若长度为10,1001&1001,结果为9
1111&1001,结果还是9,所以结果不均匀,有一些数组索引位置上永远不会有值!

HashMap的几个重要字段:如下

实际存储的key-value键值对的个数

transient int size; 

阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到

int threshold; 

负载因子,代表了table的填充度有多少,默认是0.75 加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,
等到满16个元素才扩容,某些桶里可能就有不止一个元素了。 所以加载因子默认为0.75,也就是说大小为16的HashMap,
到了第13个元素,就会扩容成32。

final float loadFactor; 

HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException

transient int modCount;

最大容量为2的30次幂,如果指定初始容量不是2的整数次幂,会通过roundUpToPowerOf2方法计算出大于初始容量的2的整数次幂值使用
JDK1.8在JDK1.7的基础上针对增加了红黑树来进行优化。即当链表超过8时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高
JDK1.8在进行元素插入时使用的是尾插法。
如果发现两个hash值(key)相同时,HashMap的处理方式是用新value替换旧value,这里并没有处理key,这正好解释了 HashMap 中没有两个相同的 key。
对NULL键的特别处理:putForNullKey()

/**
     * Offloaded version of put for null keys
     */
private V putForNullKey(V value) {
    // 若key==null,则将其放入table的第一个桶,即 table[0]
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {   
        if (e.key == null) {   // 若已经存在key为null的键,则替换其值,并返回旧值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;        // 快速失败
    addEntry(0, null, value, 0);       // 否则,将其添加到 table[0] 的桶中
    return null;
}

HashMap 中可以保存键为NULL的键值对,且该键值对是唯一的。若再次向其中添加键为NULL的键值对,将覆盖其原值。此外,如果HashMap中存在键为NULL的键值对,那么一定在第一个桶中。

HashMap 中键值对的添加:addEntry()

/**
     * Adds a new entry with the specified key, value and hash code to
     * the specified bucket.  It is the responsibility of this
     * method to resize the table if appropriate.
     *
     * Subclass overrides this to alter the behavior of put method.
     *
     * 永远都是在链表的表头添加新元素
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {


        //获取bucketIndex处的链表
        Entry<K,V> e = table[bucketIndex];


        //将新创建的 Entry 链入 bucketIndex处的链表的表头
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);


        //若HashMap中元素的个数超过极限值 threshold,则容量扩大两倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }

HashMap 总是将新的Entry对象添加到bucketIndex处,若bucketIndex处已经有了Entry对象,那么新添加的Entry对象将指向原有的Entry对象,并形成一条新的以它为链头的Entry链;但是,若bucketIndex处原先没有Entry对象,那么新添加的Entry对象将指向 null,也就生成了一条长度为 1 的全新的Entry链了。HashMap 永远都是在链表的表头添加新元素。此外,若HashMap中元素的个数超过极限值 threshold,其将进行扩容操作,一般情况下,容量将扩大至原来的两倍。

HashMap是非线程安全的
1.Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
HashMap.Size >= Capacity * LoadFactor。
2.Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。
HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

线程不安全原因分析:
线程不安全发生的主要原因是在自动扩容时进行重新hash计算时,调用了transfer函数,源码如下:

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

假如现在有一个HashMap结构如下:

(3、7、5为数组下标为1位置上的三个元素,因hash冲突生成了链表)
现在分别有线程A、B对HashMap进行扩容操作
当A执行到标红的代码时(此时未执行标红代码本行),CPU时间片用完,线程被挂起
此时A中:e.next = null, e = 3, next = 7
B线程正常执行,执行完扩容操作后HashMap结构为:

根据Java内存模式可知,线程B执行完数据迁移后,此时主内存中newTable和table都是最新的,也就是说
7.next = 3 , 3.next = null
然后继续执行线程A的后续代码,变为:
newTable[i] =3,e = 7

继续执行下一轮循环,此时7.next=3,newTable[i]=7,next=3

继续执行循环,3.next=null,newTable[i]=3,next=null,执行后停止循环

到此线程A、B的扩容操作完成,很明显当线程A执行完后,HashMap中出现了环形结构,当在以后对该HashMap进行操作时会出现死循环。
并且从上图可以发现,元素5在扩容期间被莫名的丢失了,这就发生了数据丢失的问题。

参考链接:

https://blog.csdn.net/justloveyou_/article/details/62893086
https://juejin.cn/post/6844903518331994119
https://blog.csdn.net/swpu_ocean/article/details/88917958

HashMap在JDK1.8前后的变化

参考:https://blog.csdn.net/weixin_44141495/article/details/108402128

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值