jdk源码学习

本文详细介绍了HashMap的工作原理,包括null键值处理、数据结构初始化、键值对的获取与插入流程、迭代器使用及扩容机制。此外,还探讨了ConcurrentHashMap的内部实现,涵盖其初始化、红黑树特性、树化策略与扩容控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

HashMap:

https://segmentfault.com/a/1190000012926722

1、HashMap 允许 null 键和 null 值,在计算哈键的哈希值时,null 键哈希值为 0。

2、而底层的数据结构则是延迟到插入键值对时再进行初始化。

3、默认初始化大小是16(DEFAULT_INITIAL_CAPACITY = 1 << 4;)

4、如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程

5、获取某个key值对应的value值,分三步骤:

①、首先获取第一个元素,如果该元素是要找的值就返回,否则转到②

②、继续遍历元素,首先判断节点类型,若是红黑树则用取红黑树的元素方法获取,否则③

③、继续遍历,如果有元素则返回,否则最后返回null。

总是先检查第一个节点,不仅要判断hash值是否相等,还需要判断key值,另外,如果key值不相等不代表该元素不存在,因为key可能是一个对象(不是基本数据类型),这时候就需要用equals()判断。

6、具体类中也可以定义抽象内部类。

7、Node节点的定义

8、迭代器

遍历所有的键时,首先要获取键集合KeySet对象,然后再通过 KeySet 的迭代器KeyIterator进行遍历。KeyIterator 类继承自HashIterator类,核心逻辑也封装在 HashIterator 类中。HashIterator 的逻辑并不复杂,在初始化时,HashIterator 先从桶数组中找到包含链表节点引用的桶。然后对这个桶指向的链表进行遍历。遍历完成后,再继续寻找下一个包含链表节点引用的桶,找到继续遍历。找不到,则结束遍历。

①、在构造函数中寻找第一个有元素的桶位

②、判断当前指针指向的对象是否有值

③、调用next()函数取值,并判断该桶位是否还有值,若没有,这要寻找下一个有值得桶位。

9、插入过程

首先肯定是先定位要插入的键值对属于哪个桶,定位到桶后,再判断桶是否为空。如果为空,则将键值对存入即可。如果不为空,则需将键值对接在链表最后一个位置,或者更新键值对。这就是 HashMap 的插入流程,是不是觉得很简单。当然,大家先别高兴。这只是一个简化版的插入流程,真正的插入流程要复杂不少。首先 HashMap 是变长集合,所以需要考虑扩容的问题。其次,在 JDK 1.8 中,HashMap 引入了红黑树优化过长链表,这里还要考虑多长的链表需要进行优化,优化过程又是怎样的问题。引入这里两个问题后,大家会发现原本简单的操作,现在略显复杂了。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
        else if (p instanceof TreeNode)  
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 对链表进行遍历,并统计链表长度
            for (int binCount = 0; ; ++binCount) {
                // 链表中不包含要插入的键值对节点时,则将该节点接在链表的最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果链表长度大于或等于树化阈值,则进行树化操作
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                
                // 条件为 true,表示当前链表包含要插入的键值对,终止遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 判断要插入的键值对是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 键值对数量超过阈值时,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

插入操作:

①、当桶数组 table 为空时,通过扩容的方式初始化 table

②、判断相应桶位是否有值,若没有值,直接插入,若有值,判断首节点是否和该节点通相同Key,若有记录该节点转到⑤,否则,转到③

③、判断该节点是否是树节点,若是则调用相应的函数,否则进入④

④、遍历寻找是否有和该key值相等的节点,若没有直接插入(但是要根据链表长度决定是否将链表转为红黑树),若有直接记录节点并转到⑤

⑤、要插入的键值对已经存在,根据条件判断是否用新值替换旧值(同时通过return oldValue返回,不对size++)

⑥、插入了新节点,size++,modCount++(该值记录map集合修改的次数,作为fast-fail的判断依据),并判断键值对数量是否大于阈值,大于的话则进行扩容操作。

 

10、扩容机制

 

 

 

 

二、ConcurrentHashMap源码

https://blog.youkuaiyun.com/tp7309/article/details/76532366

重要概念

table

所有数据都存在table中,table的容量会根据实际情况进行扩容,table[i]存放的数据类型有以下3种: 
- TreeBin 用于包装红黑树结构的结点类型 
- ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一 
- Node 普通结点类型,表示链表头结点

nextTable

扩容时用于存放数据的变量,扩容完成后会置为null。

sizeCtl

以volatile修饰的sizeCtl用于数组初始化与扩容控制,它有以下几个值:

if table未完成初始化:
    =0  //未指定初始容量时的默认值
    >0  //指定初始容量(非传入值,是2的幂次修正值)大小的两倍
    =-1 //表明table正在初始化
else if nextTable为空:
    if 扩容时发生错误(如内存不足、table.length * 2 > Integer.MAX_VALUE等):
        =Integer.MAX_VALUE    //不必再扩容了!
    else:
        =table.length * 0.75  //扩容阈值调为table容量大小的0.75倍
else:
    =-(1+N)  //N的低RESIZE_STAMP_SHIFT位表示参与扩容线程数,后面详细介绍

/*
     * Encodings for Node hash fields. See above for explanation.
     */
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

一、put()函数

流程:

首先判断是否已经初始化,若没有初始化,则初始化。反之,则判断当前桶位是否存在元素, 若不存在,则直接插入(先tabAt取元素,然后casTabAt()更新元素),

(然后再判断是否在扩容,若是则将当前线程用于帮助扩容)

若存在,则锁定该桶位的元素,利用hash值是否大于0判断元素节点类型,若是链表节点(fh>0),则遍历,若是存在该key的键值对,则替换,不存在则加在最后的位置。若是红黑树节点,则调用相应方法,返回该节点的引用,替换值。

再继续判断该桶位元素数量是否大于8,若是则需要树化。最后 更新size.

二、initTable()

initTable()值得一提的是调用时是没有加锁的,那么如何处理并发呢? 
由下面代码可以看到,当要初始化时会通过CAS操作将sizeCtl置为-1,而sizeCtl由volatile修饰,保证修改对后面线程可见。 
这之后如果再有线程执行到此方法时检测到sizeCtl为负数,这个线程就会告诉调度程序,我可以放弃对CPU的使用权让其它兄弟做做事啦!至于会不会理会就是另外一回事了~

三、TreeBin(红黑树)

性质:

  • 每个节点要么是红色,要么是黑色。
  • 根节点永远是黑色的。
  • 所有的叶节点都是空节点(即 null),并且是黑色的。
  • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
  • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

四、treeifyBin()

桶内元素超时8个时会调用到此方法。

分两种情况,若size小于64,这时候造成hash冲突的主要问题是容量太小,则不树化,直接扩容。

树化的时候先将普通链表转换为TreeNode,传入头结点hd构造红黑树。

 

 

1、key和 value均不能为null.

2、final V putVal(K key, V value, boolean onlyIfAbsent)

 因为hashMap都是延迟初始化,所以首先要检查tab是否为空或者length为0,如果是则初始化。

如果该桶位没有元素直接插入,

 

五、TreeBin构造函数

super(TREEBIN, null, null, null);  将(根)节点声明为红黑树节点。

先将标记根节点(this.first = b;),然后判断根节点是否空,若是则完成红黑树根节点的插入,否则遍历查找新节点的位置,首先判断方向,dir=-1是左子节点,最后若左子节点或右子节点为空则在p下添加新结点,否则p的值更新为子节点继续查找。红黑树中结点p.left <= p <= p.right x.parent = xp; //保存新结点的父结点

六 、扩容

什么时候会扩容?

  1. 使用put()添加元素时会调用addCount(),内部检查sizeCtl看是否需要扩容。
  2. tryPresize()被调用,此方法被调用有两个调用点: 
    • 链表转红黑树(put()时检查)时如果table容量小于64,则会触发扩容。
    • 调用putAll()之类一次性加入大量元素,会触发扩容。

1、addCount()

addCount()与tryPresize()实现很相似,我们先以addCount()分析下扩容逻辑:

首先统计当前map的容量大小,若大于sc,表示需要扩容, 

然后判断sc是否小于0,若是表示 已有线程在扩容,则将新线程加入扩容(调用transfer()函数,但是注意传入nextTab,避免重复初始化),否则直接开始扩容。

 

 

>>>”运算符所作的是无符号的位移处理,它不会将所处理的值的最高位视为正负符号,所以作位移处理时,会直接在空出的高位填入0。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值